Pythonic OOP

来源:AI 领域中的培养与开发 ——by 丁宁

1 关于双下划线

1.1 怎么读

  • _name:single underscore name
  • __name:double underscore name
  • __name__:dunder name
  • __init__(): dunder init method(function)

1.2 双下划线开头和结尾的变量或方法叫什么

  • 类别:special;magic;dunder
  • 实体:attribute;method

1.3 如何认识 Python 的 special method(摘自官方文档)

  • special method:method with special name(dunder)
  • Why use it?:A class can implement certain operations that are invoked by special syntax
  • original intention of design:operator overloading

Allowing classes to define their own behavior with respect to language operators.

个人补充:尽可能的保证 Python 行为的一致性(Special Method 更像是一种协议)

2 从语言设计层面理解 Python 的数据模型

2.1 一切都是对象

  • Python 中的数据模型是 Objects
  • Python 程序中所有数据都是用 objects 或者 objects 之间的关系表示的
  • 甚至 Python 代码都是 objects

2.2 Objects 的组成1:identity(标识)

  • 当 objects 创建后,identity 再也不会改变直到被销毁
  • id()is 关注的就是一个 object 的 identity
a = 1
print(id(a))
a = 1.1
print(id(a))
a = 1
print(id(a))
b = 1
print(id(b))
c = 1.1
print(id(c))

下面是在 Ipython 中的结果:

1582197824
1547884955568
1582197824
1582197824
1547884955784

要点:

  1. 变量存的是创建的 object 的 identity
  2. 创建出来的不同的 object 有不同的 identity
  3. 变量的 id 变了不是因为 object 的 identity 变了,而是对应的 object 变了
  4. 对于 immutable object,计算结果如果已经存在可直接返回相同的 identity

注意:由于浮点数并不精确,所以上面的 1.1 的两个引用的 id 的不同的。

2.2 Objects 的组成2:type

  • 当 objects 创建后,其 type 也是不会改变的
  • type() 函数返回一个 Object 的 type
  • type 决定了了一个 object 支持哪些运算,可能的值在什么范围内

2.3 Objects 的组成3:value

  • 有些 Objects 的 value 是可以改变的:mutable object
  • 有些 Objects 的 value 是不能改变的:immutable object
  • 需要注意当一个 Object 是个 container 的情况
  • 一个 Object 的 type 决定了它是否 mutable

2.4 存放其他 Object 的 reference 的 Object :Container

  • 当我们聚焦于 Container 的 values 时,我们关注的是 value
  • 当我们聚焦于 Container 的 mutability 时,关注的是 identity

3 Pythonic OOP with Special Method and Attribute

3.1 The implicit superclass – object & type object

  • 每一个 class 在定义的时候如果没有继承,都会隐式继承 object 这个 superclass
  • 每一个自定义的 class 在 Python 中都是一个 typeobject 的子类)
class X:
    pass


class Y(X):
    pass


def main():
    x = X()
    y = Y()
    print(x.__class__.__name__)
    print(y.__class__.__name__)
    print(X.__class__.__name__)
    print(Y.__class__.__name__)
    print(x.__class__.__base__.__name__)
    print(y.__class__.__base__.__name__)
    print(X.__class__.__base__.__name__)
    print(Y.__class__.__base__.__name__)


if __name__ == "__main__":
    main()
X
Y
type
type
object
X
object
object

3.2 Integrating Seamlessly with Python

class X:
    pass


class Y:
    """Class Y"""

    def __str__(self):
        return "{} object".format(self.__class__.__name__)

    def __len__(self):
        return 10

    def __bool__(self):
        return False


def check_bool(x):
    if x:
        print("I'm {}. My bool value is True.".format(str(x)))
    else:
        print("I'm {}. My bool value is False.".format(str(x)))


def main():
    x = X()
    y = Y()
    print(x)
    print(y)
    # print(len(x))
    print(len(y))
    check_bool(x)
    check_bool(y)
    print(X.__doc__)
    print(Y.__doc__)


if __name__ == "__main__":
    main()
<__main__.X object at 0x0000016866469438>
Y object
10
I'm <__main__.X object at 0x0000016866469438>. My bool value is True.
I'm Y object. My bool value is False.
None
Class Y

要点:

  1. 之所以要实现 special method,是为了让自定义的 class 与 Python 的内置函数无缝衔接
  2. Python 有⼤大量的内置函数,而这些函数大部分都是调用的对象里的 special method
  3. 想查看 Python 中到底有多少 special method见 A Guide to Python's Magic Methods。

4 Attribute Access and Properties

Attribute 相关的操作一般有:

  • Create
  • Read
  • Update
  • Delete

数据库领域中的 CRUD 亦是如此

4.1 Level I:Basic Access(Default Access)

class X:
    pass


if __name__ == "__main__":
    # print(X.a)
    X.a = "a"
    print(X.a)
    X.a = "aa"
    print(X.a)
    del X.a
    # print(X.a)

说明:

  1. 默认情况下,CRUD 都⽀支持,而且是在 public 情况下都支持(除了双下划线开头的)
  2. 如果对象中没有这个 Attribute,访问时会报错

4.2 Level II:Property(长得像 Attribute 的 Method)(推荐使用)

Property 的设计初衷:

  • 代码复用
  • 延迟计算
  • 更加规范的对象属性访问管理

场景:我要减肥,需要监控 BMI 指标,但是只能测量体重,每天更新体重,隔几天看一次 BMI 指数

class X:
    def __init__(self, w, h):
        self.w = w  # 体重
        self.h = h  # 身高
        self.BMI = w / h**2


def main():
    x = X(75, 1.83)
    print(x.BMI)
    x.w = 74
    x.w = 73
    x.w = 72
    print(x.BMI)


if __name__ == "__main__":
    main()

输出:

22.395413419331717
22.395413419331717

并没有得到预期输出,原因是 __init__ 只执行一次。

改进⼀:

class X:
    def __init__(self, w, h):
        self.__w = w
        self.__h = h
        self.BMI = w / h**2

    def update_w(self, w):
        self.__w = w
        self._update_bmi()

    def _update_bmi(self):
        self.BMI = self.__w / self.__h**2


def main():
    x = X(75, 1.83)
    print(x.BMI)
    x.update_w(74)
    x.update_w(73)
    x.update_w(72)
    print(x.BMI)


if __name__ == "__main__":
    main()

便可得到预期输出:

22.395413419331717
21.49959688255845

分析:

  1. w 变为私有,更新需要通过对象方法类执行,并将 BMI 的更新放于其中,实现功能逻辑
  2. BMI 属性依旧可以被外部访问和修改
  3. w 相关的代码全部被更改
  4. 无论 BMI 属性是否被访问,每次 w 更新均会更新 BMI,造成一定的计算资源浪费

改进二:

class X:
    def __init__(self, w, h):
        self.w = w
        self.h = h

    def get_bmi(self):
        return self.w / self.h**2


def main():
    x = X(75, 1.83)
    print(x.get_bmi())
    x.w = 74
    x.w = 73
    x.w = 72
    print(x.get_bmi())


if __name__ == "__main__":
    main()

分析:

  1. 保持 wh 属性可以随意更改,BMI 指数仅在被访问时实时计算出结果
  2. 访问 BMI 的方式由属性改为方法,造成一定程度上的代码修改
  3. w 更新频率高于 BMI 访问频率时,节省了计算资源
  4. w 未更新,却多次调用 BMI 指数时,造成了重复计算

改进三:

class X:
    def __init__(self, w, h):
        self._w = w  # 单下划线是告诉读者,最好不要改变
        self._h = h
        self._bmi = w / h**2

    def get_w(self):
        return self._w

    def set_w(self, value):
        if value <= 0:
            raise ValueError("Weight below 0 is not possible.")
         self._w = value
         self._bmi = self._w / self._h**2

    def get_bmi(self):
        return self._bmi

    w = property(get_w, set_w)
    BMI = property(get_bmi)


def main():
    x = X(75, 1.83)
    print(x.BMI)
    x.w = 74
    x.w = 73
    x.w = 72
    print(x.BMI)


if __name__ == "__main__":
    main()

分析:

  1. 通过 Property 对象显式的控制属性的访问
  2. 仅在 w 被更改的时候更新BMI,充分避免了重复计算
  3. 很容易的增加了异常处理,对更新属性进行预检验
  4. 完美复用原始调用代码,在调用方不知情的情况完成功能添加

说明:

  1. Property 对象声明必须在访问控制函数之后,否则无法创建 Property 对象
  2. 这里的写法是为了深⼊入剖析 Property 对象,其实一般不这样写,有更加优雅的写法

改进四:

class X:
    def __init__(self, w, h):
        self._w = w
        self._h = h
        self._bmi = w / h**2

    @property
    def w(self):
        return self._w

    @w.setter
    def w(self, value):
        if value <= 0:
            raise ValueError("Weight below 0 is not possible.")
        self._w = value
        self._bmi = self._w / self._h**2

    @property
    def BMI(self):
        return self._bmi


def main():
    x = X(75, 1.83)
    print(x.BMI)
    x.w = 74
    x.w = 73
    x.w = 72
    print(x.BMI)


if __name__ == "__main__":
    main()

说明:

  1. 从改进三中我们发现,传给 Property 对象的其实是函数名,那么优雅的方式当属Decorator

关于 Property 的用法

  • property(fget=None, fset=None, fdel=None, doc=None)
  • 使用 @Property 默认实现了可读
  • @Property 装饰过的 method 可以通过 @method.setter 继续装饰单输入参数方法实现可写

5 Cross-Cutting and Duck Typing (Python 设计理念)

5.1 单继承 vs 多态

  • 单继承保证了纵向的复用和一致性
  • 多态保证了跨类型的复用和一致性

5.2 传统 OOP vs 鸭子类型

  • 传统 OOP 基于类别进行设计,从类别出发逐步扩展
  • 鸭子类型仅考虑功能,从需要满足的功能出发进行设计

5.3 传统 OOP 的多态 vs 鸭子类型的多态

  • 传统 OOP 中的多态大多基于共同的基类进行设计
  • Python 中的鸭子类型无需考虑继承关系,实现了某个通用的接口就可以完成多态设计(Special Method)

5.4 MixIn:基于鸭子类型的视角看 Multi-Inheritance

class X:
    def f1():
        pass


class Y(X):
    def f2():
        pass


class A:
    def f3():
        pass


def do_f1(x):
    x.f1()


def do_f2(x):
    x.f2()


def do_f3(x):
    x.f3()


class Z(Y, A):
    pass

MixIn 的设计用中文讲比较贴切的应该是:赋能

5.5 Cross-Cutting:基于鸭子类型的视角看 Decorator 与 Special Method

  • 通过给一个类实现一个个的 Special Method,你就让这个类越来越像 Python 的 Built-in Class
  • 实现 Special Method 是从语言衔接层面为你的 Class 赋能
  • 实现 Decorator 是从通用的函数功能层面为你的 Class 赋能
  • 通过 Multi-Inheritance,利用 MixIn 的理念,你可以为你的 Class 批量化的赋能

Python 语言设计的三个关键词(其实是一件事的三个不同视角)

  • Duck Typing
  • Consistency
  • 赋能
Pythonic OOP_第1张图片

你可能感兴趣的:(Pythonic OOP)