对于你定义的每一个类,Python 会计算出一个方法解析顺序(Method Resolution Order, MRO)列表,它代表了类继承的顺序, 得益于它,Python获得很强的多重继承能力. 我们可以使用下面的方式获得某个类的MRO 列表:
新式类
ClassA.mro()(py3 使用) /ClassA.__mro__(py2 使用)
经典类
Inspect.getmro(A)
经典类
经典类本质上是一种没有继承功能的类别,实例的类型都是 type 类型,如果经典
类被算作父类,子类调用父类的构造函数会出错误,见下图:
经典类使用的是深度优先搜索(子节点顺序,从左到右)DFS,优缺点如下:
优点
如果两个互不相关的类进行多继承,正常
缺点
如果是菱形类, 会造成父类的方法覆盖子类重写的方法,该方法对某条链上的子类只能继承不能重写
这种缺点出现的原因是DFS在菱形继承中,必定在一个子类之前经过公共父类, 即公共父类A有一些方法,子类B 重写了这些方法, 但是对于B的子类D来说,B 重写的方法无法调用到,还是调用的A类方法。
比如如下例子
通过 inspect 可以看到整个类的继承顺序为 D->B->A->C,继承树是这样的
看起来好像没毛病,但是这种深度遍历带来的问题如缺点所说,A 在 C 之前加入继承链, 如果C覆盖了一个A的方法,那么覆盖的方法无法被调用到
例子,本来我们希望的是调用C.func,但是调用到了A.func()
总的来说,如果是菱形继承(存在公共父类的多继承),这种情况下 DFS 必定在一个子类之前经过公共父类,这个时候就会出现公共父类的方法覆盖另外一条链上子类方法的情况
新式类
为了让内置类型更加统一,在 2.2 引入了新式类,每一个新式类都继承与一个基类 object。
2.2 中新式类使用的是 BFS(广度优先搜索,从左到右)(新式类通过 A.__mro__查看),之前的例子同
样的代码,换用新式类,可以看到重写的方法已经变成了 C 的 func,mro 的顺序也变成了广度优先的 D->B->C->A(广度优先从左到右)
类型冲突
这种 MRO 方式已经能够解决「菱形继承」问题,再让我们看个稍微复杂点的例子:
>>> class X(object): pass
>>> class Y(object): pass
>>> class A(X, Y): pass
>>> class B(Y, X): pass
>>> class C(A, B): pass
首先进行深度遍历,结果为 [C, A, X, object, Y, object, B, Y, object, X, object];然后,只保留重复元素的最后一个,结果为 [C, A, B, Y, X, object]。Python 2.2 在实现该方法的时候进行了调整,使其更尊重基类中类出现的顺序,其实际结果为 [C, A, B, X, Y, object]。
这样的结果是否合理呢?首先我们看下各个类中的方法解析顺序:对于 A 来说,其搜索顺序为 [A, X, Y, object];对于 B,其搜索顺序为 [B, Y, X, object];对于 C,其搜索顺序为 [C, A, B, X, Y, object]。我们会发现,B 和 C 中 X、Y 的搜索顺序是相反的!也就是说,当 B 被继承时,它本身的行为竟然也发生了改变,这很容易导致不易察觉的错误。此外,即使把 C 搜索顺序中 X 和 Y 互换仍然不能解决问题,这时候它又会和 A 中的搜索顺序相矛盾。
事实上,不但上述特殊情况会出现问题,在其它情况下也可能出问题。其原因在于,上述继承关系违反了线性化的「 单调性原则 」。Michele Simionato对单调性的定义为:
A MRO is monotonic when the following is true: if C1 precedes C2 in the linearization of C, then C1 precedes C2 in the linearization of any subclass of C. Otherwise, the innocuous operation of deriving a new class could change the resolution order of methods, potentially introducing very subtle bugs.
MRO单调性的定义是,如果C1在继承链条上先于C2, 那么在任何子类的继承链条上C1都领先于C2,这种性质确保了在派生任何一个新类过程中, 方法解析顺序的改变都是无害的,从而避免了一些微妙的Bug
也就是说,子类不能改变基类的方法搜索顺序。在 Python 2.2 的 MRO 算法中并不能保证这种单调性,它不会阻止程序员写出上述具有二义性的继承关系,因此很可能成为错误的根源。
除了单调性之外,Python 2.2 及 经典类的 MRO 也可能违反继承的「 局部优先级 」,具体例子可以参见官方文档。采用一种更好的 MRO 方式势在必行。
总结
优点:解决了父亲类可能覆盖子类方法的问题
缺点:不满足单调性
新式类+C3算法
为了解决上述py2.2采用广度优先后出现的问题,py2.3 以后对新式类的继承链使用了 C3 算法,C3 算法解决了上述两个缺点,同时直接禁止了上述例子代码,直接抛出错误,及时的避免代码出现二义性, 从而保证了从子类到父类的单调性。
举个栗子
从这张图来看,感觉继承链条回到的深度优先搜索,但其实不是这样的,我们可以用一个更复杂的继承树来解释这个算法的真正力量。
class A(object):
pass
class B(object):
pass
class C(object):
pass
class D(A,C):
pass
class E(C,B):
pass
class F(D,E):
pass
print(F.mro())
[, , , , , , ]
Process finished with exit code 0
粗略一看,似乎回到了深度优先老路子,但其实注意观察E的位置, 如果使用老式广度优先算法E应该是在第三位被解析,而现在情况不是这样的.
这个例子基于C3算法算出的继承顺序如下,建议先跳过这部分,去看看后面的C3算法的解析(看完以后试着把E继承变成E(B,C),再推导一下)
L(O) = O
L(A) = A +merge( L(O) )= A, O
L(B) = B+ merge(L(O) )= B O
L(C) = C +merge( L(O) )= C, O
L(D) = D + merge(L(A),L(C))
-> D + A + ([O], [C, O])
->D + A + C + O
L(E) = E + C + B+ O
L(F) = F + (D + A + C + O, E + C + B+ O)
-> F + D + ( A + C + O, E + C + B+ O)
-> F + D + A + (C + O, E + C + B+ O)
-> F + D + A + (C + O, E + C + B+ O)
-> F + D + A + E + (C + O, C + B+ O)
-> F + D + A + E + C + B+ O
对官方例子的一个解读
>>> O = object
>>> class X(O): pass
>>> class Y(O): pass
>>> class A(X,Y): pass
>>> class B(Y,X): pass
-----------
| |
| O |
| / \ |
- X Y /
| / | /
| / |/
A B
\ /
C
在Python2.3中(MRO使用C3的第一个版本),这种继承行为会抛出一个错误(TypeError: MRO conflict among bases Y, X)
因为Python解释器禁止了(天真的)程序员创建这种带有歧义的继承结构,但是py2.2不会抛错,解析顺序按照广度优先为CABXYO
首先我们定义一下术语
对于一个类的列表C1 C2 ... CN(in python =[C1, C2, ... , CN])
head = C1
tail = C2 ... CN.
C + (C1 C2 ... CN) = C C1 C2 ... CN
定义函数L(C),L表示对C和C父类线性队列进行合并L[C(B1 ... BN)] = C + merge(L[B1] ... L[BN], B1 ... BN)
其中 L[object] = object
merge的规则
1.从第一个列表里面取出head, 如果这个head不在其他任务队列的tail部分,那么将其加入到解析顺序队列, 然后从该列表中移除它,重复直到列表为空.
2.如果该head是其他队列的tail部分,那么看下一个list,重复`1`操作
3.重复操作直到所有的类都被移除完毕,或者在遍历了所有列表后任然无法找到一个合适head
4.如果所有的列表在被遍历后都无法找到一个合适的head,在py2.3+会抛出错误
以上面的图为例子
L(O) = O
L(X) = X +merge( L(O) )= X, O
L(Y) = Y+ merge(L(O) )= Y, O
L(A) = A + merge(L(X) , L(Y)) = A, X, Y ,O
L(B) = B + merge(L(Y) , L(X)) = B, Y, X ,O
L(C) = C + merge(L(A) , L(B))
-> C + ( [A, X, Y ,O] , [B, Y, X ,O])
-> C +A + ( [X, Y ,O] , [B, Y, X ,O])
-> C +A + ( [X, Y ,O] , [B, Y, X ,O]) 尝试取出X,但X不是一个好Head,跳到下一个列表
-> C + A + B + ( [X, Y ,O] , [ Y, X ,O]) 错误,这样的列表无法找到一个合适的头
如果只有一个父亲类,那么,就不会有这些困扰了
L[C(B)] = C + merge(L[B],B) = C + L[B]