流畅的python——9 符合 python 风格的对象

九、符合 python 风格的对象

绝对不要使用两个前导下划线,这是很烦人的自私行为。

​ ——Ian Bicking

​ pip、virtualenv 和 Paste 等项目的创建者

得益于 python 数据模型,自定义类型的行为可以像内置类型那样自然。实现如此自然的行为,靠的不是继承,而是鸭子类型:我们只需要按照预定行为实现对象所需的方法即可。

对象表示形式

repr()

以便于开发者理解的方式返回对象的字符串表示形式

str()

以便于用户理解的方式返回对象的字符串表示形式

bytes()

获取对象的字节序列表示形式

format , str.format __format__

使用特殊的格式代码显示对象的字符串表示形式

例子:向量类

In [1]: from array import array

In [2]: import math

In [3]: class Vector2d:
   ...:     typecode = 'd'
   ...:     def __init__(self,x,y):
   ...:         self.x = float(x)  # 尽早捕获异常,防止传入参数不当
   ...:         self.y = float(y)
   ...:     def __iter__(self):  # 可迭代,可拆包,生成器
   ...:         return (i for i in (self.x,self.y))
   ...:     def __repr__(self):
   ...:         class_name = type(self).__name__
    			# 感叹号后面跟的是conversion,而conversion有两个值.
				# 分别是s对应str()函数, r对应repr()函数。
   ...:         return '{}({!r},{!r})'.format(class_name, *self)  # 可迭代,*self 拆包
   ...:     def __str__(self):
   ...:         return str(tuple(self))
   ...:     def __bytes__(self):
   ...:         return (bytes([ord(self.typecode)]) + bytes(array(self.typecode, self)))
   ...:     def __eq__(self,other):
   ...:         return tuple(self) == tuple(self)
   ...:     def __abs__(self):
   ...:         return math.hypot(self.x,self.y)
   ...:     def __bool__(self):
   ...:         return bool(abs(self))

 	...:     @classmethod
    ...:     def frombytes(cls, b):
    ...:         typecode = chr(b[0])
    ...:         memv = memoryview(b[1:]).cast(typecode)  # 将共享内存数据转为 'd' 类型
    ...:         return cls(*memv)
    ...:     def __format__(self,fmt_spec = ''):  # 以 p 结尾的 格式说明符,返回向量形式
    ...:         # compnents = (format(c,fmt_spec) for c in self)
    ...:         #return '({}, {})'.format(*compnents)
    ...:         if fmt_spec.endswith('p'):
    ...:             fmt_spec = fmt_spec[:-1]
    ...:             coords = (abs(self), self.angle())
    ...:             outer_fmt = '<{}, {}>'
    ...:         else:
    ...:             coords = self
    ...:             outer_fmt = '({}, {})'
    ...:         components = (format(c, fmt_spec) for c in coords)
    ...:         return outer_fmt.format(*components)
    ...:
    ...:     def angle(self):
    ...:         return math.atan2(self.y,self.x)

classmethod

classmethod 最常见的用途是定义备选构造方法。

staticmethod

静态方法就是定义在类中的普通函数。

格式化显示

__format__(format_spec)

format_spec 是格式说明符

1 format(my_obj, format_spec)

2 str.format() 方法的格式字符串,{} 里代换字段中冒号后面的部分

In [22]: format(brl, '0.4f')  # 格式说明符是 0.4f
Out[22]: '0.4274'

IIn [23]: '1 BRL = {rate:0.2f} US'.format(rate=brl)  # 格式说明符是 0.2f
# 代换字段中的 'rate' 子串是字段名称,与格式说明符无关,但是它决定把 .format() 的哪个参数传给代换字段。
Out[23]: '1 BRL = 0.43 US'

第 2 条标注指出了一个重要知识点:'{0.mass:5.3e}' 这样的格式字符串其实包含两部分,冒号左边的 '0.mass' 在代换字段句法中是字段名冒号后面的 '5.3e' 是格式说明符。格式说明符使用的表示法叫格式规范微语言(“Format Specification Mini Language”,https://docs.python.org/3/library/string.html#formatspec)。

datetime 模块中的类,它们的 __format__·方法使用的格式代码与 strftime() 函数一样。

>>> from datetime import datetime
>>> now = datetime.now()
>>> format(now, '%H:%M:%S')
'18:49:05'
>>> "It's now {:%I:%M %p}".format(now)
"It's now 06:49 PM"

如果类没有定义 __format__ 方法,从 object 继承的方法,会返回 str(my_obj)

然而,如果传入格式说明符, obj.__format__ 会抛出异常。

In [24]: format(a)
Out[24]: '(1.0, 2.0)'

In [25]: format(a,'.3f')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-25-75412ed59581> in <module>
----> 1 format(a,'.3f')

TypeError: unsupported format string passed to Vector2d.__format__

自定义 p 结尾,为向量形式

>>> format(Vector2d(1, 1), 'p')
'<1.4142135623730951, 0.7853981633974483>'
>>> format(Vector2d(1, 1), '.3ep')
'<1.414e+00, 7.854e-01>'
>>> format(Vector2d(1, 1), '0.5fp')
'<1.41421, 0.78540>'

可散列的 Vector2d

当前对象不可散列

In [40]: a
Out[40]: Vector2d(7.0,8.0)

In [41]: b = {a : 1}
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-41-12275979062c> in <module>
----> 1 b = {a : 1}

TypeError: unhashable type: 'Vector2d'
        
In [42]: hash(a)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-42-57b555d30865> in <module>
----> 1 hash(a)

TypeError: unhashable type: 'Vector2d'

什么是可散列的数据类型

如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现 __hash__() 方法。另外可散列对象还要有 __qe__() 方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的……

根据特殊方法 __hash__的文档(https://docs.python.org/3/reference/datamodel.html),最好使用位运算符异或(^)混合各分量的散列值——我们会这么做。

In [45]: class Vector2d:
    ...:     typecode = 'd'
    ...:     def __init__(self,x,y):
    ...:         self.__x = float(x)
    ...:         self.__y = float(y)
    ...:     @property
    ...:     def x(self):
    ...:         return self.__x
    ...:     @property
    ...:     def y(self):
    ...:         return self.__y
    ...:     def __iter__(self):
    ...:         return (i for i in (self.x, self.y))
    ...:     def __hash__(self):
    ...:         return hash(self.x) ^ hash(self.y)
In [57]: a = Vector2d(7,8)

In [58]: a
Out[58]: <__main__.Vector2d at 0x274a4874cc0>

In [59]: hash(a)
Out[59]: 15

In [60]: b = Vector2d(1,1)

In [61]: hash(b)
Out[61]: 0

要想创建可散列的类型,不一定要实现特性,也不一定要保护实例属性。只需要正确实现 __hash____eq__ 方法即可。但是,实例的散列值绝对不应该变化。

如果定义的类型有标量数值,可能还要实现 __init____float__ 方法(分别 被 intfloat 构造函数调用),以便在某些情况下用于强制转换类型。此外,还有用于支持内置的 complex() 构造函数的 __complex__ 方法。

Python 的私有属性和“受保护的”属性

私有属性:__mood 会存入实例的 __dict__ 属性中,变成 _Dog__mood ,且会在前面加上一个下划线和类名。这个语言特性叫 名称改写。

名称改写是一种安全措施,不能保证万无一失:它的目的是避免意外访问,不能防止故意做错事。

只要编写 v1._Vector__x = 7 这样的代码,就能轻松地为 Vector2d 实例的私有分量直接赋值。如果真在生产环境中这么做了,出问题时可别抱怨。

不过在模块中,顶层名称使用一个前导下划线的话,的确会有影响:对 from mymod import * 来说,mymod 中前缀为下划线的名称不会被导入。然而,依旧可以使用 from mymod import _privatefunc 将其导入。Python 教程的 6.1节“More on Modules”(https://docs.python.org/3/tutorial/modules.html#more-on-modules)说明了这一点。

Python 文档的某些角落把使用一个下划线前缀标记的属性称为“受保护的”属性。使用self._x 这种形式保护属性的做法很常见,但是很少有人把这种属性叫作“受保护的”属性。有些人甚至将其称为“私有”属性。

总之,Vector2d 的分量都是“私有的”,而且 Vector2d 实例都是“不可变的”。我用了两对引号,这是因为并不能真正实现私有和不可变。

使用 __slots__ 类属性节省空间

默认情况下,python 在各个实例中名为 __dict__ 的字典里存储实例属性。为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果属性过多,用 __slots__ 存储属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不是用字典。

继承自超类的 __slots__ 属性没有效果。Python 只会使用各个类中定义的 __slots__ 属性。

定义 __slots__ 的方式是,创建一个类属性,使用 __slots__ 这个名字,并把它的值设为一个字符串构成的可迭代对象,其中各个元素表示各个实例属性。我喜欢使用元组,因为这样定义的 __slots__ 中所含的信息不会变化。

class Vector2d:
    __slots__ = ('__x', '__y')
    typecode = 'd'

在类中定义 __slots__ 属性的目的是告诉解释器:“这个类中的所有实例属性都在这儿了!”这样,Python 会在各个实例中使用类似元组的结构存储实例变量,从而避免使用消耗内存的 __dict__ 属性。如果有数百万个实例同时活动,这样做能节省大量内存。

如果要处理数百万个数值对象,应该使用 NumPy 数组。NumPy 数组能高效使用内存,而且提供了高度优化的数值处理函数,其中很多都一次操作整个数组。

在类中定义 __slots__ 属性之后,实例不能再有 __slots__ 中所列名称之外的其他属性。这只是一个副作用,不是 __slots__ 存在的真正原因。不要使用 __slots__ 属性禁止类的用户新增实例属性。__slots__ 是用于优化的,不是为了约束程序员。

然而,“节省的内存也可能被再次吃掉”:如果把 '__dict__' 这个名称添加到 __slots__ 中,实例会在元组中保存各个实例的属性,此外还支持动态创建属性,这些属性存储在常规的 __dict__ 中。当然,把 '__dict__' 添加到 __slots__ 中可能完全违背了初衷,这取决于各个实例的静态属性和动态属性的数量及其用法。粗心的优化甚至比提早优化还糟糕。

此外,还有一个实例属性可能需要注意,即 __weakref__ 属性,为了让对象支持弱引用(参见 8.6 节),必须有这个属性。用户定义的类中默认就有 __weakref__ 属性。可是,如果类中定义了 __slots__ 属性,而且想把实例作为弱引用的目标,那么要把 '__weakref__' 添加到__slots__ 中。

综上,__slots__ 属性有些需要注意的地方,而且不能滥用,不能使用它限制用户能赋值的属性。处理列表数据时 __slots__ 属性最有用,例如模式固定的数据库记录,以及特大型数据集。

__slots__ 的问题

1 每个子类都要定义 __slots__ 属性,因为解释器会忽略继承的 __slots__ 属性。

2 实例只能拥有 __slots__ 中列出的属性,除非把 '__dict__' 加入 __slots__ 中(这样做就失去了节省内存的功效)。

3 如果不把 '__weakref__' 加入 __slots__,实例就不能作为弱引用的目标。

如果你的程序不用处理数百万个实例,或许不值得费劲去创建不寻常的类,那就禁止它创建动态属性或者不支持弱引用。与其他优化措施一样,仅当权衡当下的需求并仔细搜集资料后证明确实有必要时,才应该使用 __slots__属性。

覆盖类属性

Python 有个很独特的特性:类属性可用于为实例属性提供默认值。但是,如果为不存在的实例属性赋值,会新建实例属性。假如我们为 typecode 实例属性赋值,那么同名类属性不受影响。然而,自此之后,实例读取的 self.typecode 是实例属性 typecode,也就是把同名类属性遮盖了。借助这一特性,可以为各个实例的 typecode 属性定制不同的值。

如果想修改类属性的值,应该直接 类.属性 而不是 self.属性,修改类属性,可以修改所有实例的类属性。

然而,有种修改方法更符合 Python 风格,而且效果持久,也更有针对性。类属性是公开的,因此会被子类继承,于是经常会创建一个子类,只用于定制类的数据属性。

这也说明了我在 Vecto2d.__repr__ 方法中为什么没有硬编码 class_name 的值,而是使用 type(self).__name__ 获取。如果硬编码 class_name 的值,那么 Vector2d 的子类(如 ShortVector2d)要覆盖__repr__ 方法,只是为了修改 class_name 的值。从实例的类型中读取类名,__repr__ 方法就可以放心继承。

__index__ 这个方法的作用是强制把对象转换成整数索引,在特定的序列切片场景中使用,以及满足 NumPy 的一个需求。

要构建符合 Python 风格的对象,就要观察真正的 Python 对象的行为。 ——古老的中国谚语

特性有助于减少前期投入

python 类和实例的所有属性都是公开的,当要避免意外修改了属性,可以实现特性,调用方式不受影响。

java 没有特性,只能实现读值方法和设值方法。然而,这些方法必须写,但不能保证有用。

维基的发明人和极限编程先驱 Ward Cunningham 建议问这个问题:“做这件事最简单的方法是什么?”意即,我们应该把焦点放在目标上。提前实现设值方法和读值方法偏离了目标。在 Python 中,我们可以先使用公开属性,然后等需要时再变成特性。

私有属性的安全性和保障性

Perl 不会强制你保护隐私。你应该待在客厅外,因为你没收到邀请,而不是因为里面有把枪。

​ ——Larry Wall | Perl 之父

java 提供好的隐私保障,但是只有用 SecurityManager 部署时,才是绝对隐私的。但是,实际并不常用。

所以,java 的隐私属性也是一种防止意外的措施。通常也不是绝对安全的,也是可以修改的。

所以,隐私属性并非绝对隐私,而是一种防止意外的措施,是一种约定俗成的规范。

作者:我的观点是,Java 中的访问控制修饰符基本上也是安全措施,不能保证万无一失——至少实践中是如此。因此,安心享用 Python 提供的强大功能吧,放心去用吧!

原文地址:https://www.cnblogs.com/pythonwl/p/15344599.html