目录
1 MRO简介
对于支持继承的编程语言来说,其方法(属性)可能定义在当前类,也可能来自于基类,所以在方法调用时就需要对当前类和基类进行搜索以确定方法所在的位置。而搜索的顺序就是所谓的「方法解析顺序」(Method Resolution Order,或MRO)。对于只支持单继承的语言来说,MRO 一般比较简单;而对于 Python 这种支持多继承的语言来说,MRO 就复杂很多。
先看一个「菱形继承」的例子:
如果 x 是 D 的一个实例,那么 x.show() 到底会调用哪个 show 方法呢?如果按照 [D, B, A, C] 的搜索顺序,那么 x.show() 会调用 A.show();如果按照 [D, B, C, A] 的搜索顺序,那么 x.show() 会调用 C.show()。由此可见,MRO 是把类的继承关系线性化的一个过程,而线性化方式决定了程序运行过程中具体会调用哪个方法。既然如此,那什么样的 MRO 才是最合理的?Python 中又是如何实现的呢?
Python 至少有三种不同的 MRO:
- 经典类(classic class)的深度遍历。
- Python 2.2 的新式类(new-style class)预计算。
- Python 2.3 的新式类的C3 算法。它也是 Python 3 唯一支持的方式。
2 经典类的 MRO
Python 有两种类:经典类(classic class)和新式类(new-style class)。两者的不同之处在于新式类继承自 object。在 Python 2.1 以前,经典类是唯一可用的形式;Python 2.2 引入了新式类,使得类和内置类型更加统一;在 Python 3 中,新式类是唯一支持的类。
经典类采用了一种很简单的 MRO 方法:从左至右的深度优先遍历。这个过程具体如下:
- 检查当前的类里面是否有该函数,如果有则直接调用。
- 检查当前类的第一个父类里面是否有该函数,如果没有则检查父类的第一个父类是否有该函数,以此递归深度遍历。
- 如果没有则回溯一层,检查下一个父类里面是否有该函数并按照 2 中的方式递归。
上面的过程与标准的深度优先遍历只有一点细微的差别:步骤 2 总是按照继承列表中类的先后顺序来选择分支的遍历顺序。
以上述「菱形继承」为例,其查找顺序为 [D, B, A, C, A],如果只保留重复类的第一个则结果为 [D,B,A,C]。我们可以用 inspect.getmro 来获取类的 MRO:
>>> import inspect
>>> class A:
... def show(self):
... print "A.show()"
...
>>> class B(A): pass
>>> class C(A):
... def show(self):
... print "C.show()"
...
>>> class D(B, C): pass
>>> inspect.getmro(D)
(<class __main__.D at 0x105f0a6d0>, <class __main__.B at 0x105f0a600>, <class __main__.A at 0x105f0a668>, <class __main__.C at 0x105f0a738>)
>>> x = D()
>>> x.show()
A.show()
这种深度优先遍历对于简单的情况还能处理的不错,但是对于上述「菱形继承」其结果却不尽如人意:虽然 C.show() 是 A.show() 的更具体化版本(显示了更多的信息),但我们的 x.show() 没有调用它,而是调用了 A.show()。这显然不是我们希望的结果。
对于新式类而言,所有的类都继承自 object,所以「菱形继承」是非常普遍的现象,因此不可能采用这种 MRO 方式。
3 Python 2.2 的新式类 MRO
为解决经典类 MRO 所存在的问题,Python 2.2 针对新式类提出了一种新的 MRO 计算方式:在定义类时就计算出该类的 MRO 并将其作为类的属性。因此新式类可以直接通过__mro__属性获取类的 MRO。
Python 2.2 的新式类 MRO 计算方式和经典类 MRO 的计算方式非常相似:它仍然采用从左至右的深度优先遍历,但是如果遍历中出现重复的类,只保留最后一个。重新考虑上面「菱形继承」的例子,由于新式类继承自 object 因此类图稍有改变[新式类菱形继承]:
按照深度遍历,其顺序为 [D, B, A, object, C, A, object],重复类只保留最后一个,因此变为 [D, B, C, A, object]。代码为:
>>> class A(object):
... def show(self):
... print "A.show()"
...
>>> class B(A): pass
>>> class C(A):
... def show(self):
... print "C.show()"
...
>>> class D(B, C): pass
>>> D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <type 'object'>)
>>> x = D()
>>> x.show()
C.show()
这种 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.
也就是说,子类不能改变基类的方法搜索顺序。在 Python 2.2 的 MRO 算法中并不能保证这种单调性,它不会阻止程序员写出上述具有二义性的继承关系,因此很可能成为错误的根源。
除了单调性之外,Python 2.2 及 经典类的 MRO 也可能违反继承的「 局部优先级 」,具体例子可以参见官方文档。采用一种更好的 MRO 方式势在必行。
4 C3 MRO(C3 linearization)
在介绍算法之前,我们首先约定需要使用的符号。我们用 表示包含 N 个类的列表,并令
为了方便做列表连接操作,我们记:
假设类 C 继承自父类 ,那么根据 C3 线性化,类 C 的方法解析列表通过如下公式确定:
这个公式表明 C 的解析列表是通过对其所有父类的解析列表及其父类一起做 merge 操作所得到。
接下来我们介绍 C3 线性化中最重要的操作 merge,该操作可以分为以下几个步骤:
- 选取 merge 中的第一个列表记为当前列表 K。
- 令 h=head(K),如果 h 没有出现在其他任何列表的 tail 当中,那么将其加入到类 C 的线性化列表中,并将其从 merge 中所有列表中移除,之后重复步骤 2。
- 否则,设置 K 为 merge 中的下一个列表,并重复 2 中的操作。
- 如果 merge 中所有的类都被移除,则输出类创建成功;如果不能找到下一个 h,则输出拒绝创建类 C 并抛出异常。
上面的过程看起来好像很复杂,我们用例子来具体执行一下,你就会觉得其实还是挺简单的。
4.1 例子一
假设我们有如下的一个类继承关系:
class X():
def who_am_i(self):
print("I am a X")
class Y():
def who_am_i(self):
print("I am a Y")
class A(X, Y):
def who_am_i(self):
print("I am a A")
class B(Y, X):
def who_am_i(self):
print("I am a B")
class F(A, B):
def who_am_i(self):
print("I am a F")
首先我们有 L[X]=X,L[Y]=Y,然后立即可以得到:
根据公式:
我们首先选取 h=head(L[A])=A,发现 A 可以加入到类 C 的解析列表中(同时将 A 从其他列表中删去),所以得到:
之后选取 h=head(L[A])=X,发现 X 不能满足要求(因为X 在 L[B] 的 tail 中出现),所以根据步骤 3 选取下一个列表并令 h=head(L[B])=B,然后将 B 加入到类 C 的解析列表中得到:
接下来按照算法流程应该选取 h=head(L[B])=Y,很显然 Y 并不能满足要求,此时由于 merge 中没有下一个列表了,所以不能继续选择 h,所以根据步骤 4 算法输出类 F 创建失败并抛出异常。如果你用 new-style class 的方式执行一下上面的代码,你就会发现 Python 解释器将不允许你创建类 F:
Traceback (most recent call last):
File "test.py", line 17, in <module>
class F(A, B):
TypeError: Cannot create a consistent method resolution
order (MRO) for bases X, Y
4.2 例子二
计算过程如下:
L[object] = [object]
L[D] = [D, object]
L[E] = [E, object]
L[F] = [F, object]
L[B] = [B, D, E, object]
L[C] = [C, D, F, object]
L[A] = [A] + merge(L[B], L[C], [B], [C])
= [A] + merge([B, D, E, object], [C, D, F, object], [B], [C])
= [A, B] + merge([D, E, object], [C, D, F, object], [C])
= [A, B, C] + merge([D, E, object], [D, F, object])
= [A, B, C, D] + merge([E, object], [F, object])
= [A, B, C, D, E] + merge([object], [F, object])
= [A, B, C, D, E, F] + merge([object], [object])
= [A, B, C, D, E, F, object]
当然,可以用代码验证类的 MRO,上面的例子可以写作:
class D(object): pass
class E(object): pass
class F(object): pass
class B(D, E): pass
class C(D, F): pass
class A(B, C): pass
A.__mro__
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.
C3 线性化算法与 MRO,理解Python中的多继承:http://kaiyuan.me/2016/04/27/C3_linearization/
Python中的MRO(方法解析顺序):https://www.cnblogs.com/gandoufu/p/9634914.html