- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- Python 术语表
- Python 版本表
- 排版约定
- 使用代码示例
- 第一部分 序幕
- 第 1 章 Python 数据模型
- 第二部分 数据结构
- 第 2 章 序列构成的数组
- 第 3 章 字典和集合
- 第 4 章 文本和字节序列
- 第三部分 把函数视作对象
- 第 5 章 一等函数
- 第 6 章 使用一等函数实现设计模式
- 第 7 章 函数装饰器和闭包
- 第四部分 面向对象惯用法
- 第 8 章 对象引用、可变性和垃圾回收
- 第 9 章 符合 Python 风格的对象
- 第 10 章 序列的修改、散列和切片
- 第 11 章 接口:从协议到抽象基类
- 第 12 章 继承的优缺点
- 第 13 章 正确重载运算符
- 第五部分 控制流程
- 第 14 章 可迭代的对象、迭代器和生成器
- 14.1 Sentence 类第1版:单词序列
- 14.2 可迭代的对象与迭代器的对比
- 14.3 Sentence 类第2版:典型的迭代器
- 14.4 Sentence 类第3版:生成器函数
- 14.5 Sentence 类第4版:惰性实现
- 14.6 Sentence 类第5版:生成器表达式
- 14.7 何时使用生成器表达式
- 14.8 另一个示例:等差数列生成器
- 14.9 标准库中的生成器函数
- 14.10 Python 3.3 中新出现的句法:yield from
- 14.11 可迭代的归约函数
- 14.12 深入分析 iter 函数
- 14.13 案例分析:在数据库转换工具中使用生成器
- 14.14 把生成器当成协程
- 14.15 本章小结
- 14.16 延伸阅读
- 第 15 章 上下文管理器和 else 块
- 第 16 章 协程
- 第 17 章 使用期物处理并发
- 第 18 章 使用 asyncio 包处理并发
- 第六部分 元编程
- 第 19 章 动态属性和特性
- 第 20 章 属性描述符
- 第 21 章 类元编程
- 结语
- 延伸阅读
- 附录 A 辅助脚本
- Python 术语表
- 作者简介
- 关于封面
11.3 使用猴子补丁在运行时实现协议
示例 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论