Python 接口:从协议到抽象基 类(使用猴子补丁在运行时实现协议)
使用猴子补丁在运行时实现协议
示例 11-4 中的 FrenchDeck 类有个重大缺陷:无法洗牌。几年前,第
一次编写 FrenchDeck 示例时,我实现了 shuffle 方法。后来,我对
Python 风格有了深刻理解,我发现如果 FrenchDeck 实例的行为像序
列,那么它就不需要 shuffle 方法,因为已经有 random.shuffle 函
数可用,文档中说它的作用是“就地打乱序列
x”(https://docs.python.org/3/library/random.html#random.shuffle)。
如果遵守既定协议,很有可能增加利用现有的标准库和第三
方代码的可能性,这得益于鸭子类型。
标准库中的 random.shuffle 函数用法如下:
>>> from random import shuffle
>>> l = list(range(10))
>>> shuffle(l)
>>> l
[5, 2, 9, 7, 8, 3, 1, 4, 0, 6]
然而,如果尝试打乱 FrenchDeck 实例,会出现异常,如示例 11-5 所
示。
示例 11-5 random.shuffle 函数不能打乱 FrenchDeck 实例
>>> from random import shuffle
>>> from frenchdeck import FrenchDeck
>>> deck = FrenchDeck()
>>> shuffle(deck)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File ".../python3.3/random.py", line 265, in shuffle
x[i], x[j] = x[j], x[i]
TypeError: 'FrenchDeck' object does not support item assignment
错误消息相当明确,“‘FrenchDeck’ object does not support item
assignment”(‘FrenchDeck’ 对象不支持为元素赋值)。这个问题的原因是,shuffle 函数要调换集合中元素的位置,而 FrenchDeck 只实现
了不可变的序列协议。可变的序列还必须提供__setitem__ 方法。
Python 是动态语言,因此我们可以在运行时修正这个问题,甚至还可以
在交互式控制台中,修正方法如示例 11-6 所示。
示例 11-6 为FrenchDeck 打猴子补丁,把它变成可变的,让
random.shuffle 函数能处理(接续示例 11-5)
>>> def set_card(deck, position, card): ➊
... deck._cards[position] = card
...
>>> FrenchDeck.__setitem__ = set_card ➋
>>> shuffle(deck) ➌
>>> deck[:5]
[Card(rank='3', suit='hearts'), Card(rank='4', suit='diamonds'), Card(rank='4',
suit='clubs'), Card(rank='7', suit='hearts'), Card(rank='9', suit='spades')]
❶ 定义一个函数,它的参数为 deck、position 和 card。
❷ 把那个函数赋值给 FrenchDeck 类的__setitem__ 属性。
❸ 现在可以打乱 deck 了,因为 FrenchDeck 实现了可变序列协议所需
的方法。
特殊方法__setitem__ 的签名在 Python 语言参考手册的“3.3.6.
Emulating container
types”(https://docs.python.org/3/reference/datamodel.html#emulatingcontainer-
types)中定义。语言参考中使用的参数是 self、key 和
value,而这里使用的是 deck、position 和 card。这么做是为了告
诉你,每个 Python 方法说到底都是普通函数,把第一个参数命名为
self 只是一种约定。在控制台会话中使用那几个参数没问题,不过在
Python 源码文件中最好按照文档那样使用 self、key 和 value。
这里的关键是,set_card 函数要知道 deck 对象有一个名为 cards 的
属性,而且 cards 的值必须是可变序列。然后,我们把 set_card 函
数赋值给特殊方法__setitem,从而把它依附到 FrenchDeck 类
上。这种技术叫猴子补丁:在运行时修改类或模块,而不改动源码。猴
子补丁很强大,但是打补丁的代码与要打补丁的程序耦合十分紧密,而
且往往要处理隐藏和没有文档的部分。
除了举例说明猴子补丁之外,示例 11-6 还强调了协议是动态
的:random.shuffle 函数不关心参数的类型,只要那个对象实现了部
分可变序列协议即可。即便对象一开始没有所需的方法也没关系,后来
再提供也行。
目前,本章讨论的主题是“鸭子类型”:对象的类型无关紧要,只要实现
了特定的协议即可。
前面给出的抽象基类图表是为了展示协议与抽象基类的文档中所说的接
口之间的关系,但是目前为止还没有真正继承抽象基类。
在接下来的几节中,我们将直接使用抽象基类,而不只将其当作文档。