Python 支持多重继承,当一个类继承多个父类,且父类之间有复杂的继承关系时,继承的方法按什么顺序解析呢?《Fluent Python》中提到 python 解释器按照 C3 算法确定方法解析顺序(MRO,method resolution order),但作者认为:除非大量使用多重继承,或者继承关系不同寻常,否则不用了解C3算法……

本想就这么跳过去,结果看到作者书中 Tkinter 的多重继承图,瞬间头大,觉得还是有必要了解一下 C3 算法的。

本文主要基于 The Python 2.3 Method Resolution Order ,相当于用自己的话写了一遍,方便以后复习,也为懒得读英文的同学节约时间。

符号约定

对于类 C:C(A) 表示 C 继承了 A 类,C(A, B) 表示 C 继承了 A 类和 B 类,且 A 类的优先级比 B 类高。

L[C] 为 C 的方法解析顺序(MRO)。例如 C 继承了 A,那么 L[C] = CA,表示先解析C中的方法,再解析 A 中的方法。如果 L[C] = CC1C2C3...CN,则表示解析顺序是 C、C1 …… 以此类推。

head/tail:对于解析顺序 CC1C2C3...CN,它的 head 为 C,tail 为 C1...CN 。

加法运算:C+(C1C2C3...CN) = CC1C2C3...CN。

merge:C3 算法用到的一种运算,下文中将详细介绍。

C3 算法

对类 C 而言,解析序列中排第一位的毋庸置疑,一定是它自身实现的方法,即: L[C] = C + (...)。 假如类 C 继承了 B1,B2,... BN。那么,它的解析序列为:

L[C(B1, B2, B3, ..., BN)] = C+merge(L[B1], L[B2], L[B3], ... , B1B2B3...BN)

注意 merge 括号内最后加粗的部分是一项(而不是N项),和前面的 L[B1],L[B2] 之类的项是等价的,都代表一种解析序列。

上面的公式显示,类 C 的解析序列是:它本身+它的各个直系父类的解析序列,以及它的直系父类在继承列表中的排列序列的融合(merge)。

下面进入算法的核心部分:merge。

merge 运算首先按顺序遍历其内部各项的 head,如果第一项的 head 不出现在其它各项的 tail 中,那么就把这个 head 提取到 merge 外面;反之,如果第一项的 head 出现在后面某项的 tail 中,那么就不能提取它,而应检验第二项的 head,以此类推,直到找到某项的 head,它不出现在任何一项的 tail 中,把这个 head 提取到 merge 的外部。一旦提取出某个 head,对于 merge 内部的各项,则需要删掉这个提取到外面的 head,并继续对剩下的部分做 merge。

举例说明

merge(ABC, DBC) 中,第一项的 head 为 A,第二项 DBC 的 tail 中不包含 A,那么就把 A 提出来,并把 merge 内部所有的 A 都删掉:

merge(ABC, DBC) = A + merge(BC, DBC)

merge(BC, DBC) 中,第一项的 head 为 B,但是第二项的 tail 中包含 B,B 不应该被提取出来。继而研究第二项的 head D,D 不出现在任何一项的 tail 中,因此应该把 D 提取出来,并把 merge 内部所有的 D 都删掉:

merge(ABC, DBC) = A + merge(BC, DBC) = A+D+merge(BC, BC) = AD+merge(BC, BC)

接下来很简单了:

merge(BC, BC) = B+merge(C,C) = B+C = BC
综上,merge(ABC, DBC) = ADBC

代码示例

我们不妨构建这样的继承关系:

O = object
class F(O): pass
class E(O): pass
class D(O): pass
class C(D, F): pass
class B(E, D): pass
class A(B, C): pass

D、E、F 继承了 O;C 先后继承了 D、F;B 先后继承了 E、D;最终 A 继承了 B、C。如下过程展示了 C3 算法推导类 A 的方法解析顺序。

首先,D、E、F 的 mro (method resolution order) 是明确的:

L[D] = DO
L[E] = EO
L[F] = FO
B、C 的 mro 则的稍微复杂一点:
L[B(E,D)] = B+merge(L[E], L[D], ED) # merge 内部不要忘记最后一项是继承序列
= B+merge(EO,DO,ED) # 代入各直系父类的解析序列
= B+E+merge(O,DO,D) # 提取出 E,并删掉 merge 中的 E
= B+E+D+merge(O,O) # 第一项的 head O 出现在了第二项的 tail 中,故提取第二项的head
= B+E+D+O
= BEDO
L[C(D,F)] = C+merge(L[D], L[F], DF)
= C+merge(DO,FO,DF)
= C+D+merge(O,FO,F)
= C+D+F+merge(O,O)
= CDFO
A 的 mro 基于上面的 L[B] 和 L[C]:
L[A(B,C)]=A+merge(L[B], L[C], BC)
= A+merge(BEDO, CDFO, BC)
= A+B+merge(EDO,CDFO,C)
= A+B+E+merge(DO,CDFO,C)
= A+B+E+C+merge(DO,DFO)
= A+B+E+C+D+merge(O,FO)
= A+B+E+C+D+F+O
= ABECDFO
代码验证一下:
A.__mro__
"""(__main__.A,__main__.B,__main__.E,__main__.C,__main__.D,__main__.F,object)"""

错误的继承

上文介绍了如何根据 C3 算法推导复杂继承情况下的 MRO,从 python2.3 开始,方法解析顺序都遵循 C3 算法。这就限制了某些继承关系是不能发生的,例如:

O = object
class X(O): pass
class Y(O): pass
class A(X, Y): pass
class B(Y, X): pass
class C(A, B): pass
"""TypeError Traceback (most recent call last) in 4 class A(X, Y): pass5 class B(Y, X): pass----> 6 class C(A, B): passTypeError: Cannot create a consistent method resolutionorder (MRO) for bases X, Y"""

上述类的继承图为:

按照 C3 算法推导 C 的 MRO:
L[X(O)] = XO
L[Y(O)] = YO
L[A(X,Y)] = A+merge(L[X], L[Y], XY)
= A+merge(XO,YO,XY)
= A+X+merge(O,YO,Y)
= A+X+Y+merge(O,O)
= AXYO
L[B(Y,X)] = B+merge(L[Y], L[X], YX)
= B+merge(YO, XO, YX)
= B+Y+merge(O, XO, X)
= B+Y+X+merge(O,O)
= BYXO
L[C(A,B)]=C+merge(L[A], L[B], AB)
= C+merge(AXYO, BYXO, AB)
= C+A+merge(XYO, BYXO, B)
= C+A+B+merge(XYO,YXO)

推导到上面的最后一步,我们发现 merge 内部,不论是第一项 XYO 还是第二项 YXO,他们的 head 都是另一项的 tail,因此 C3 算法到这一步就卡住了,python2.3 往上的版本都会报错:TypeError: Cannot create a consistent method resolution。

Tkinter

最后,我们不妨再回过头来推导一下 Tkinter 下 Text 类的方法解析顺序

我们根据上图(红线揭示了并列父类的继承顺序),可以判断其继承关系是这样的(首字母代表类名,首字母相同的用前两个字母代表类名):

object(O) YView(Y) XView(X) Grid(G) Place(Pl)
Pack(Pa) Misc(M) BaseWeight(B) Widget(W) Text(T)
L[Y] = YO, L[X] = XO, L[G] = GO, L[Pl] = PlO, L[Pa]=PaO, L[M]=MO
L[B(M)] = B+L[M] = BMO
L[W(B,M,Pa,Pl,G)] = W+merge(L[B], L[M], L[Pa], L[Pl], L[G])
= W+merge(BMO, MO, PaO, PlO,GO)
= W+B+merge(MO,MO,PaO,PlO,GO)
= WB+M+merge(O,O,PaO,PlO,GO)
=WBM+Pa+merge(O,O,O,PlO,GO)
=WBMPaPlGO
L[T(W,X,Y)] = T+merge(L[W], L[X], L[Y])
= T+merge(WBMPaPlGO, XO, YO)
= T+WBMPaPlG+merge(O,XO,YO)
= TWBMPaPlGXYO

上面的推导结果正是图中红线标示的方法解析顺序。