任何实现多重继承的语言都要处理潜在的命名冲突,这种冲突由不相关的祖先类实现同名方法引起。这种冲突称为“菱形问题”,如图 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”(https://www.python.org/download/releases/2.3/mro/)对 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__
下一节以真实框架为例说明多重继承的优缺点。