- 前言
- 目标读者
- 非目标读者
- 本书的结构
- 以实践为基础
- 硬件
- 杂谈:个人的一点看法
- 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.2 多重继承和方法解析顺序
任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关的祖先类实现同名方法引起。这种冲突称为“菱形问题”,如图 12-1 和示例 12-4 所示。
图 12-1:(左)说明“菱形问题”的 UML 类图;(右)虚线箭头是示例 12-4 使用的方法解析顺序
示例 12-4 diamond.py:图 12-1 中的 A、B、C 和 D 四个类
class A: def ping(self): print('ping:', self) class B(A): def pong(self): print('pong:', self) class C(A): def pong(self): print('PONG:', self) class D(B, C): def ping(self): super().ping() print('post-ping:', self) def pingpong(self): self.ping() super().ping() self.pong() super().pong() C.pong(self)
注意,B 和 C 都实现了 pong 方法,二者之间唯一的区别是,C.pong 方法输出的是大写的 PONG。
在 D 的实例上调用 d.pong() 方法的话,运行的是哪个 pong 方法呢?在 C++ 中,程序员必须使用类名限定方法调用来避免这种歧义。Python 也能这么做,如示例 12-5 所示。
示例 12-5 在 D 实例上调用 pong 方法的两种方式
>>> from diamond import * >>> d = D() >>> d.pong() # ➊ pong: <diamond.D object at 0x10066c278> >>> C.pong(d) # ➋ PONG: <diamond.D object at 0x10066c278>
❶ 直接调用 d.pong() 运行的是 B 类中的版本。
❷ 超类中的方法都可以直接调用,此时要把实例作为显式参数传入。
Python 能区分 d.pong() 调用的是哪个方法,是因为 Python 会按照特定的顺序遍历继承图。这个顺序叫方法解析顺序(Method Resolution Order,MRO)。类都有一个名为 __mro__ 的属性,它的值是一个元组,按照方法解析顺序列出各个超类,从当前类一直向上,直到 object 类。D 类的 __mro__ 属性如下(如图 12-1 所示):
>>> D.__mro__ (<class 'diamond.D'>, <class 'diamond.B'>, <class 'diamond.C'>, <class 'diamond.A'>, <class 'object'>)
若想把方法调用委托给超类,推荐的方式是使用内置的 super() 函数。在 Python 3 中,这种方式变得更容易了,如示例 12-4 中 D 类的 pingpong 方法所示。4 然而,有时可能需要绕过方法解析顺序,直接调用某个超类的方法——这样做有时更方便。例如,D.ping 方法可以这样写:
4在 Python 2 中,要把 D.pingpong 方法的第二行从 super().ping() 改成 super(D, self).ping()。
def ping(self): A.ping(self) # 而不是super().ping() print('post-ping:', self)
注意,直接在类上调用实例方法时,必须显式传入 self 参数,因为这样访问的是未绑定方法(unbound method)。
然而,使用 super() 最安全,也不易过时。调用框架或不受自己控制的类层次结构中的方法时,尤其适合使用 super()。使用 super() 调用方法时,会遵守方法解析顺序,如示例 12-6 所示。
示例 12-6 使用 super() 函数调用 ping 方法(源码在示例 12-4 中)
>>> from diamond import D >>> d = D() >>> d.ping() # ➊ ping: <diamond.D object at 0x10cc40630> # ➋ post-ping: <diamond.D object at 0x10cc40630> # ➌
❶ D 类的 ping 方法做了两次调用。
❷ 第一个调用是 super().ping();super 函数把 ping 调用委托给 A 类;这一行由 A.ping 输出。
❸ 第二个调用是 print('post-ping:', self),输出的是这一行。
下面来看在 D 实例上调用 pingpong 方法得到的结果,如示例 12-7 所示。
示例 12-7 pingpong 方法的 5 个调用(源码在示例 12-4 中)
>>> from diamond import D >>> d = D() >>> d.pingpong() ping: <diamond.D object at 0x10bf235c0> # ➊ post-ping: <diamond.D object at 0x10bf235c0> ping: <diamond.D object at 0x10bf235c0> # ➋ pong: <diamond.D object at 0x10bf235c0> # ➌ pong: <diamond.D object at 0x10bf235c0> # ➍ PONG: <diamond.D object at 0x10bf235c0> # ➎
❶ 第一个调用是 self.ping(),运行的是 D 类的 ping 方法,输出这一行和下一行。
❷ 第二个调用是 super().ping(),跳过 D 类的 ping 方法,找到 A 类的 ping 方法。
❸ 第三个调用是 self.pong(),根据 __mro__ ,找到的是 B 类实现的 pong 方法。
❹ 第四个调用是 super().pong(),也根据 __mro__ ,找到 B 类实现的 pong 方法。
➎ 第五个调用是 C.pong(self),忽略 mro ,找到的是 C 类实现的 pong 方法。
方法解析顺序不仅考虑继承图,还考虑子类声明中列出超类的顺序。也就是说,如果在 diamond.py 文件(见示例 12-4)中把 D 类声明为 class D(C, B):,那么 D 类的 __mro__ 属性就会不一样:先搜索 C 类,再搜索 B 类。
分析类时,我经常在交互式控制台中查看 __mro__ 属性。示例 12-8 中是一些常用类的方法搜索顺序。
示例 12-8 查看几个类的 __mro__ 属性
>>> bool.__mro__ ➊ (<class 'bool'>, <class 'int'>, <class 'object'>) >>> def print_mro(cls): ➋ ... print(', '.join(c.__name__ for c in cls.__mro__)) ... >>> print_mro(bool) bool, int, object >>> from frenchdeck2 import FrenchDeck2 >>> print_mro(FrenchDeck2) ➌ FrenchDeck2, MutableSequence, Sequence, Sized, Iterable, Container, object >>> import numbers >>> print_mro(numbers.Integral) ➍ Integral, Rational, Real, Complex, Number, object >>> import io ➎ >>> print_mro(io.BytesIO) BytesIO, _BufferedIOBase, _IOBase, object >>> print_mro(io.TextIOWrapper) TextIOWrapper, _TextIOBase, _IOBase, object
❶ bool 从 int 和 object 中继承方法和属性。
❷ print_mro 函数使用更紧凑的方式显示方法解析顺序。
❸ FrenchDeck2 类的祖先包含 collections.abc 模块中的几个抽象基类。
❹ 这些是 numbers 模块提供的几个数字抽象基类。
❺ io 模块中有抽象基类(名称以 ...Base 后缀结尾)和具体类,如 BytesIO 和 TextIOWrapper。open() 函数返回的对象属于这些类型,具体要根据模式参数而定。
方法解析顺序使用 C3 算法计算。Michele Simionato 的论文“The Python 2.3 Method Resolution Order”对 Python 方法解析顺序使用的 C3 算法做了权威论述。如果对方法解析顺序的细节感兴趣,可以阅读延伸阅读中给出的资料。不用过分担心,C3 算法不难理解,Simionato 写道:
……除非大量使用多重继承,或者继承关系不同寻常,否则不用了解 C3 算法,因此也不用阅读这篇论文。
结束对方法解析顺序的讨论之前,我们来看看图 12-2。这幅图展示了 Python 标准库中 GUI 工具包 Tkinter 复杂的多重继承图。研究这幅图时,要从底部的 Text 类开始。这个类全面实现了多行可编辑文本小组件,它自身有丰富的功能,不过也从其他类继承了很多方法。左边是常规的 UML 类图。右边加入了一些箭头,表示方法解析顺序。使用示例 12-8 中定义的便利函数 print_mro 得到的输出如下:
>>> import tkinter >>> print_mro(tkinter.Text) Text, Widget, BaseWidget, Misc, Pack, Place, Grid, XView, YView, object
图 12-2:(左)Tkinter 中 Text 小组件类及其超类的 UML 类图;(右)使用虚线箭头表示 Text.__mro__
下一节以真实框架为例说明多重继承的优缺点。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论