- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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 术语表
- 作者简介
- 关于封面
12.1 子类化内置类型很麻烦
在 Python 2.2 之前,内置类型(如 list 或 dict)不能子类化。在 Python 2.2 之后,内置类型可以子类化了,但是有个重要的注意事项:内置类型(使用 C 语言编写)不会调用用户定义的类覆盖的特殊方法。
PyPy 的文档使用简明扼要的语言描述了这个问题,见于“Differences between PyPy and CPython”中“Subclasses of built-in types”一节:
至于内置类型的子类覆盖的方法会不会隐式调用,CPython 没有制定官方规则。基本上,内置类型的方法不会调用子类覆盖的方法。例如,dict 的子类覆盖的 __getitem__() 方法不会被内置类型的 get() 方法调用。
示例 12-1 说明了这个问题。
示例 12-1 内置类型 dict 的 __init__ 和 __update__ 方法会忽略我们覆盖的 __setitem__ 方法
>>> class DoppelDict(dict): ... def __setitem__(self, key, value): ... super().__setitem__(key, [value] * 2) # ➊ ... >>> dd = DoppelDict(one=1) # ➋ >>> dd {'one': 1} >>> dd['two'] = 2 # ➌ >>> dd {'one': 1, 'two': [2, 2]} >>> dd.update(three=3) # ➍ >>> dd {'three': 3, 'one': 1, 'two': [2, 2]}
❶ DoppelDict.__setitem__ 方法会重复存入的值(只是为了提供易于观察的效果)。它把职责委托给超类。
❷ 继承自 dict 的 __init__ 方法显然忽略了我们覆盖的 __setitem__ 方法:'one' 的值没有重复。
❸ [] 运算符会调用我们覆盖的 __setitem__ 方法,按预期那样工作:'two' 对应的是两个重复的值,即 [2, 2]。
❹ 继承自 dict 的 update 方法也不使用我们覆盖的 __setitem__ 方法:'three' 的值没有重复。
原生类型的这种行为违背了面向对象编程的一个基本原则:始终应该从实例(self)所属的类开始搜索方法,即使在超类实现的类中调用也是如此。在这种糟糕的局面中,__missing__ 方法(参见 3.4.2 节)却能按预期方式工作,不过这只是特例。
不只实例内部的调用有这个问题(self.get() 不调用 self.__getitem__()),内置类型的方法调用的其他类的方法,如果被覆盖了,也不会被调用。示例 12-2 是一个例子,改编自 PyPy 文档中的示例。
示例 12-2 dict.update 方法会忽略 AnswerDict.__getitem__ 方法
>>> class AnswerDict(dict): ... def __getitem__(self, key): # ➊ ... return 42 ... >>> ad = AnswerDict(a='foo') # ➋ >>> ad['a'] # ➌ 42 >>> d = {} >>> d.update(ad) # ➍ >>> d['a'] # ➎ 'foo' >>> d {'a': 'foo'}
❶ 不管传入什么键,AnswerDict.__getitem__ 方法始终返回 42。
❷ ad 是 AnswerDict 的实例,以 ('a', 'foo') 键值对初始化。
❸ ad['a'] 返回 42,这与预期相符。
❹ d 是 dict 的实例,使用 ad 中的值更新 d。
❺ dict.update 方法忽略了 AnswerDict.__getitem__ 方法。
直接子类化内置类型(如 dict、list 或 str)容易出错,因为内置类型的方法通常会忽略用户覆盖的方法。不要子类化内置类型,用户自己定义的类应该继承 collections 模块中的类,例如 UserDict、UserList 和 UserString,这些类做了特殊设计,因此易于扩展。
如果不子类化 dict,而是子类化 collections.UserDict,示例 12-1 和示例 12-2 中暴露的问题便迎刃而解了。参见示例 12-3。
示例 12-3 DoppelDict2 和 AnswerDict2 能像预期那样使用,因为它们扩展的是 UserDict,而不是 dict
>>> import collections >>> >>> class DoppelDict2(collections.UserDict): ... def __setitem__(self, key, value): ... super().__setitem__(key, [value] * 2) ... >>> dd = DoppelDict2(one=1) >>> dd {'one': [1, 1]} >>> dd['two'] = 2 >>> dd {'two': [2, 2], 'one': [1, 1]} >>> dd.update(three=3) >>> dd {'two': [2, 2], 'three': [3, 3], 'one': [1, 1]} >>> >>> class AnswerDict2(collections.UserDict): ... def __getitem__(self, key): ... return 42 ... >>> ad = AnswerDict2(a='foo') >>> ad['a'] 42 >>> d = {} >>> d.update(ad) >>> d['a'] 42 >>> d {'a': 42}
为了衡量子类化内置类型所需的额外工作量,我做了个实验,重写了示例 3-8 中的 StrKeyDict 类。原始版继承自 collections.UserDict,而且只实现了三个方法:__missing__、__contains__ 和 __setitem__。在实验中,StrKeyDict 直接子类化 dict,而且也实现了那三个方法,不过根据存储数据的方式稍微做了调整。可是,为了让实验版通过原始版的测试组件,还要实现 __init__、get 和 update 方法,因为继承自 dict 的版本拒绝与覆盖的 __missing__、__contains__ 和 __setitem__ 方法合作。示例 3-8 中那个 UserDict 子类有 16 行代码,而实验的 dict 子类有 37 行代码。2
2如果好奇,实验版在本书代码仓库里的 strkeydict_ dictsub.py 文件中。
综上,本节所述的问题只发生在 C 语言实现的内置类型内部的方法委托上,而且只影响直接继承内置类型的用户自定义类。如果子类化使用 Python 编写的类,如 UserDict 或 MutableMapping,就不会受此影响。3
3顺便说一下,在这方面,PyPy 的行为比 CPython“正确”,不过会导致微小的差异。详情参见“Differences between PyPy and CPython”。
与继承,尤其是多重继承有关的另一个问题是:如果同级别的超类定义了同名属性,Python 如何确定使用哪个?下一节解答。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论