Python最好的品质之一是一致性。
数据模型所描述的API,为使用最地道的语言特性来构建你自己的对象提供了工具。
数据模型其实是对Python框架的描述,它规范了这门语言自身构建模块的接口,这些模块包括但不限于序列、迭代器、函数、类和上下文管理器。
Python解释器碰到特殊的句法时,会使用特殊方法去激活一些基本的对象操作,这些特殊方法的名字以两个下划线开头,以两个下划线结尾。比如 obj[key] 的背后就是 __getitem__ 方法,为了能求得 my_collection[key] 的值,解释器实际上会调用 my_collection.__getitem__(key) 。
1.1 一摞Python风格的纸牌
纸牌类的代码:
1 | import collections |
namedtuple用以构建只有少数属性但是没有方法的对象。用以下代码可以得到一个纸牌对象:
1 | beer_card = Card('7', 'diamonds') |
可以用len() 函数来查看一叠牌有多少张:
1 | deck = FrenchDeck() |
从一叠牌中抽取特定的一张纸牌,这是由 __getitem__ 方法提供的:
1 | >>> deck[0] |
使用Python内置的函数 random.choice 从一叠牌中随机选出一个元素:
1 | from random import choice |
通过实现特殊方式来利用Python数据模型的好处:
- 作为类的用户,他们不必去记住标准操作的各式名称,因为每种操作都有固定的函数名称。
- 更加方便地利用Python地标准库,比如
random.choice函数,从而不用重复发明轮子。
使用切片操作查看最上面3张和查看牌面是A的牌:
1 | deck[:3] |
使用for循环迭代这一摞牌:
1 | for card in deck: |
反向迭代:
1 | for card in reversed(deck): |
迭代通常是隐式的,当集合类型没有实现 __contains__ 方法,那么in运算符就会按顺序做一次迭代搜索:
1 | Card('Q', 'hearts') in deck |
用sorted对这摞牌进行升序排序:
1 | suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0) |
FrenchDeck类通过数据模型和一些合成来实现这些功能。通过实现 __len__ 和 __getitem__ 这两个特殊方法,FrenchDeck就跟一个Python 自有的序列数据类型一样,可以体现出Python 的核心语言特性(例如迭代和切片)。同时这个类还可以用于标准库中诸如random.choice、reversed 和sorted 这些函数。另外,对合成的运用使得 __len__ 和 __getitem__ 的具体实现可以代理给self._cards 这个Python 列表(即list 对象)。
1.2 如何使用特殊方法
特殊方法的存在是为了被Python解释器调用的,你自己并不需要调用它们。也就是说没有 my_object.__len__() 这种写法,而应该使用len(my_object)。
很多时候,特殊方法的调用是隐式的,比如for i in x: 这个语句,背后其实用的是iter(x),而这个函数的背后则是 x.__iter__() 方法。当然前提是这个方法在x 中被实现了。
通过内置的函数(例如len、iter、str,等等)来使用特殊方法是最好的选择。这些内置函数不仅会调用特殊方法,通常还提供额外的好处,而且对于内置的类来说,它们的速度更快。
1.2.1 模拟数值类型
利用特殊方法,可以让自定义对象通过加号“+”(或是别的运算符)进行运算。
二维向量类:
1 | from math import hypot |
向量加法:
1 | v1 = Vector(2, 4) |
abs函数可以返回输入复数的模:
1 | v = Vector(3, 4) |
用*运算符来实现向量的标量乘法(即向量与数的乘法,得到的结果向量的方向与原向量一致,模变大)
1 | v * 3 |
1.2.2 字符串表示形式
repr 就是通过 __repr__ 这个特殊方法来得到一个对象的字符串表示形式的。如果没有实现 __repr__ ,当我们在控制台里打印一个向量的实例时,得到的字符串可能会是<Vector object at 0x10e100070>。
交互式控制台和调试程序(debugger)用repr 函数来获取字符串表示形式;在老的使用%符号的字符串格式中,这个函数返回的结果用来代替%r 所代表的对象;同样,str.format函数所用到的新式字符串格式化语法也是利用了repr,才把!r 字段变成字符串。
在 __repr__ 的实现中,我们用到了%r 来获取对象各个属性的标准字符串表示形式——这是个好习惯,它暗示了一个关键:Vector(1, 2) 和Vector(‘1’, ‘2’) 是不一样的。
__repr__ 所返回的字符串应该准确、无歧义,并且尽可能表达出如何用代码创建出这个被打印的对象,比如Vector(3, 4)。
__repr__ 和 __str__ 的区别在于,后者是在str() 函数被使用,或是在用print 函数打印一个对象的时候才被调用的,并且它返回的字符串对终端用户更友好。
如果你只想实现这两个特殊方法中的一个,__repr__ 是更好的选择,因为如果一个对象没有 __str__ 函数,而Python 又需要调用它的时候,解释器会用 __repr__ 作为替代。
1.2.3 算术运算符
通过 __add__ 和 __mul__ ,上面的向量类带来了+ 和* 这两个算术运算符。值得注意的是,这两个方法的返回值都是新创建的向量对象,被操作的两个向量(self 或other)还是原封不动,代码里只是读取了它们的值而已。中缀运算符的基本原则就是不改变操作对象,而是产出一个新的值。
1.2.4 自定义的布尔值
尽管Python 里有bool 类型,但实际上任何对象都可以用于需要布尔值的上下文中(比如if 或while 语句,或者and、or 和not 运算符)。为了判定一个值x 为真还是为假,Python会调用bool(x),这个函数只能返回True 或者False。
bool(x) 的背后是调用 x.__bool__() 的结果;如果不存在 __bool__ 方法,那么bool(x) 会尝试调用 x.__len__() 。若返回0,则bool 会返回False;否则返回True。
1.3 特殊方法一览
Python 语言参考手册中的“Data Model”一章列出了83 个特殊方法的名字,其中47 个用于实现算术运算、位运算和比较操作。
跟运算符无关的特殊方法
| 类别 | 方法名 |
|---|---|
| 字符串/ 字节序列表示形式 | __repr__、__str__、__format__、__bytes__ |
| 数值转换 | __abs__、__bool__、__complex__、__int__、__float__、__hash__、__index__ |
| 集合模拟 | __len__、__getitem__、__setitem__、__delitem__、__contains__ |
| 迭代枚举 | __iter__、__reversed__、__next__ |
| 可调用模拟 | __call__ |
| 上下文管理 | __enter__、__exit__ |
| 实例创建和销毁 | __new__、__init__、__del__ |
| 属性管理 | __getattr__、__getattribute__、__setattr__、__delattr__、__dir__ |
| 属性描述符 | __get__、__set__、__delete__ |
| 跟类相关的服务 | __prepare__、__instancecheck__、__subclasscheck__ |
跟运算符相关的特殊方法
| 类别 | 方法名和对应的运算符 |
|---|---|
| 一元运算符 | __neg__ -、__pos__ +、__abs__ abs() |
| 众多比较运算符 | __lt__ <、__le__ <=、__eq__ ==、__ne__ !=、__gt__ >、__ge__ >= |
| 算术运算符 | __add__ +、__sub__ -、__mul__ *、__truediv__ /、__floordiv__ //、__mod__ %、__divmod__ divmod()、__pow__ ** 或pow()、__round__ round() |
| 反向算术运算符 | __radd__、__rsub__、__rmul__、__rtruediv__、__rfloordiv__、__rmod__、__rdivmod__、__rpow__ |
| 增量赋值算术运算符 | __iadd__、__isub__、__imul__、__itruediv__、__ifloordiv__、__imod__、__ipow__ |
| 位运算符 | __invert__ ~、__lshift__ <<、__rshift__ >>、__and__ &、__or__、__xor__ ^ |
| 反向位运算符 | __rlshift__、__rrshift__、__rand__、__rxor__、__ror__ |
| 增量赋值位运算符 | __ilshift__、__irshift__、__iand__、__ixor__、__ior__ |
1.4 为什么len不是普通方法
len 之所以不是一个普通方法,是为了让Python 自带的数据结构可以走后门,abs 也是同理。但是多亏了它是特殊方法,我们也可以把len 用于自定义数据类型。这种处理方式在保持内置类型的效率和保证语言的一致性之间找到了一个平衡点,也印证了“Python 之禅”中的另外一句话:“不能让特例特殊到开始破坏既定规则。”
1.5 本章小结
通过实现特殊方法,自定义数据类型可以表现得跟内置类型一样,从而让我们写出更具表达力的代码——或者说,更具Python 风格的代码。
Python 对象的一个基本要求就是它得有合理的字符串表示形式,我们可以通过 __repr__ 和 __str__ 来满足这个要求。前者方便我们调试和记录日志,后者则是给终端用户看的。这就是数据模型中存在特殊方法 __repr__ 和 __str__ 的原因。
Python 通过运算符重载这一模式提供了丰富的数值类型,除了内置的那些之外,还有decimal.Decimal 和fractions.Fraction。这些数据类型都支持中缀算术运算符。
延伸阅读
而我恰恰认为“特殊方法”是“魔术方法”的对立面。Python 和Ruby 都利用了这个概念来提供丰富的元对象协议,这不是魔术,而是让语言的用户和核心开发者拥有并使用同样的工具。
元对象协议这个词对我们学习Python 数据模型是有帮助的。元对象所指的是那些对建构语言本身来讲很重要的对象,以此为前提,协议也可以看作接口。也就是说,元对象协议是对象模型的同义词,它们的意思都是构建核心语言的API。
我对元对象协议的理解:python中,元对象是指序列、迭代器、函数、类和上下文管理器这些内置模块;元对象协议是指这些模块的接口规范,也就是特殊方法的定义规范,我们也可以定义这些特殊方法来实现自己的模块。