返回介绍

11.3 使用猴子补丁在运行时实现协议

发布于 2024-02-05 21:59:47 字数 2917 浏览 0 评论 0 收藏 0

示例 11-4 中的 FrenchDeck 类有个重大缺陷:无法洗牌。几年前,第一次编写 FrenchDeck 示例时,我实现了 shuffle 方法。后来,我对 Python 风格有了深刻理解,我发现如果 FrenchDeck 实例的行为像序列,那么它就不需要 shuffle 方法,因为已经有 random.shuffle 函数可用,文档中说它的作用是“就地打乱序列 x”。

 如果遵守既定协议,很有可能增加利用现有的标准库和第三方代码的可能性,这得益于鸭子类型。

标准库中的 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”中定义。语言参考中使用的参数是 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 函数不关心参数的类型,只要那个对象实现了部分可变序列协议即可。即便对象一开始没有所需的方法也没关系,后来再提供也行。

目前,本章讨论的主题是“鸭子类型”:对象的类型无关紧要,只要实现了特定的协议即可。

前面给出的抽象基类图表是为了展示协议与抽象基类的文档中所说的接口之间的关系,但是目前为止还没有真正继承抽象基类。

在接下来的几节中,我们将直接使用抽象基类,而不只将其当作文档。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文