
Fluent Python 第九章读书报告
Chapter 9. A Pythonic Object
第九章: Pythonic 的对象
得益于 Python 数据模型,自定义类型的行为可以像内置类型那样自然。实现如此自然的行为,靠的不是继承,而是鸭子类型(duck typing):我们只需按照预定行为实现对象所需的方法即可。
本章包含以下话题:
- 支持用于生成对象其他表示形式的内置函数(如 repr()、bytes(),等等)
- 使用一个类方法实现备选构造方法
- 扩展内置的 format() 函数和 str.format() 方法使用的格式微语言
- 实现只读属性
- 把对象变为可散列的,以便在集合中及作为 dict 的键使用
- 利用 __slots__ 节省内存
对象表示形式
Python 提供了两种方式获取对象的字符串表示形式。
- repr() 便于开发者理解的方式返回对象的字符串表示形式。
- str() 便于用户理解的方式返回对象的字符串表示形式。
为了给对象提供其他的表示形式,还会用到另外两个特殊方法:__bytes__ 和__format__。__bytes__ 方法与 __str__ 方法类似:bytes() 函数调用它获取对象的字节序列表示形式。而 __format__ 方法会被内置的 format() 函数和 str.format() 方法调用,使用特殊的格式代码显示对象的字符串表示形式.
构建一个向量类
向量类的实现如下:
1 | from array import array |
备选构造方法
上一节的vector实例可以将vector转化成字节序列,同理我们也可以将字节序列转化成vector。
vector2d_v1.py:
1 | from vector2d_v0 import Vector2d as vec |
classmethod 和 staticmethod
python 提供了两个装饰器来装饰类中定义的方法:classmethod 和 staticmethod
- classmethod 用来定义操作类而不是操作实例的方法。classmethod 最常见的方式就是定义备用的构造方法。
- staticmethod 用来定义与实例无关的一些操作,相当于定位在类中的普通函数
格式化显示
内置的 format() 函数和 str.format() 方法把各个类型的格式化方式委托给相应的.__format__(format_spec) 方法。format_spec 是格式说明符,它是:format(my_obj, format_spec) 的第二个参数,或者str.format() 方法的格式字符串,{} 里代换字段中冒号后面的部分.
1 | 1/2.43 brl = |
【1】中的’0.4f’是格式说明符
【2】中格式说明符是’0.2f’, ‘rate’是字段名称,’{0.mass:5.3e}’这样的格式中, ‘0.mass’是字段名, ‘5.3e’是格式
格式规范微语言: 格式说明符使用的表示法, 格式规范微语言是可扩展的,因为各个类可以自行决定如何解释 format_spec 参数。
首先实现一个简单的格式化方法:
1 | def __format__(self, fmt_spec=''): |
这样可以实现如下效果:
1 | 3, 4) v1 = Vector2d( |
下面增加一个自定义的格式说明符p, 如果格式说明符以 ‘p’ 结尾,那么在极坐标中显示向量,即 <r, θ>,其中 r 是模,θ是弧度
下面是实现:
1 | def angle(self): |
可散列的(hashable)Vector2d
代码见 vector2d_v3.py
目前Vector2d是不可散列的, 因此不能放入集合中,为了使得Vector2d变成可散列的,需要实现__hash__,并且让Vector2d不可变
首先需要让Vector2d不可变(使用@property装饰器装饰读值方法(getter)):
1 | def __init__(self, x, y): |
这样x和y都是只读的了。接下来实现__hash__方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24def __hash__(self):
return hash(self.x) ^ hash(self.y)
```
_注: 如果要实现一个可散列的类型,不一定要保护实例变量属性或者实现properties,只需要正确实现\_\_hash\_\_和\_\_eq\_\_即可,但是实例的hash值绝对不应该改变,所以这里会提到只读特性_
## Python中的私有属性和受保护的属性
为了避免子类覆盖父类的私有属性,如果以 \_\_mood 的形式(两个前导下划线,尾部没有或最多有一个下划线)命名实例属性,Python 会把属性名存入实例的 \_\_dict\_\_ 属性中,而且会在前面加
上一个下划线和类名。因此,对 Dog 类来说,\_\_mood 会变成 \_Dog\_\_mood;对 Beagle
类来说,会变成 \_Beagle\_\_mood。这个语言特性叫名称改写(name mangling)。
_需要注意的是, 名称改写是一种安全措施,不能保证万无一失:它的目的是避免意外访问,不能防止故意做错事_
Python 文档的某些角落把使用一个下划线前缀标记的属性称为“受保护的”属性。 使用self._x 这种形式保护属性的做法很常见,但是很少有人把这种属性叫作“受保护的”属性。有些人甚至将其称为“私有”属性。
下面继续对 Vector2d 类进行改动。在最后一节中,我们将讨论一个特殊的属性(不是方法),它会影响对象的内部存储,对内存用量可能也有重大影响,不过对对象的公开接口没什么影响。这个属性是 \_\_slots\_\_
## 使用\_\_slots\_\_类属性节省空间
默认情况下,Python 在各个实例中名为 \_\_dict\_\_ 的字典里存储实例属性。如 3.9.3 节所述,为了使用底层的散列表提升访问速度,字典会消耗大量内存。如果要处理数百万个属性不多的实例,通过 \_\_slots\_\_ 类属性,能节省大量内存,方法是让解释器在元组中存储实例属性,而不用字典。
```python
class Vector2d:
__slots__ = ('__x', '__y')
typecode = 'd'
在类中定义 __slots__ 属性的目的是告诉解释器:“这个类中的所有实例属性都在这儿了!”这样,Python 会在各个实例中使用类似元组的结构存储实例变量,从而避免使用消耗内存的 __dict__ 属性。如果有数百万个实例同时活动,这样做能节省大量内存。
总之,如果使用得当,__slots__ 能显著节省内存,不过有几点要注意。每个子类都要定义 __slots__ 属性,因为解释器会忽略继承的 __slots__ 属性。实例只能拥有 __slots__ 中列出的属性,除非把 ‘__dict__‘ 加入 __slots__ (这样做就失去了节省内存的功效)。如果不把 ‘__weakref__‘ 加入 __slots__,实例就不能作为弱引用的目标。
覆盖类属性
Python 有个很独特的特性:类属性可用于为实例属性提供默认值。
类属性是公开的,因此会被子类继承,于是经常会创建一个子类,只用于定制类的数据属性。Django基于类的视图就大量使用了这个技术。
小结
本章的目的是说明,如何使用特殊方法和约定的结构,定义行为良好且符合 Python 风格的类。
同时也提到了下面几种特殊方法的用法:
- 所有用于获取字符串和字节序列表示形式的方法:__repr__、__str__、__format__ 和 __bytes__。
- 把对象转换成数字的几个方法:__abs__、__bool__和 __hash__。
- 用于测试字节序列转换和支持散列(连同 __hash__ 方法)的 __eq__ 运算符。
提到了格式规范微语言
提到了使用__slots__节省内存
提到了使用继承的方式覆盖类属性的方法
最后:
To build Pythonic objects, observe how real Python objects behave.
— Ancient Chinese proverb(误)