Python之方法解析顺序(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:
1.经典类(classic class)的深度遍历
2.Python2.2的新式类(new-style class)预计算
3.Python2.3的新式类的C3算法。它也是Python3唯一支持的方式

经典类的 MRO
Python 有两种类:经典类(classic class)和新式类(new-style class)。两者的不同之处在于新式类继承自 object。在 Python 2.1 以前,经典类是唯一可用的形式;Python 2.2 引入了新式类,使得类和内置类型更加统一;在 Python 3 中,新式类是唯一支持的类。
经典类采用了一种很简单的 MRO 方法:从左至右的深度优先遍历。以上述「菱形继承」为例,其查找顺序为 [D, B, A, C, A],如果只保留重复类的第一个则结果为 [D, B, A, C]。我们可以用 inspect.getmro 来获取类的 MRO:

import inspect
class Test:
	pass
class Test2(Test):
	pass
inspect.getmro(Test2)

这种深度优先遍历对于简单的情况还能处理的不错,但是对于上述「菱形继承」其结果却不尽如人意:虽然 C.show() 是 A.show() 的更具体化版本(显示了更多的信息),但我们的x.show() 没有调用它,而是调用了 A.show()。这显然不是我们希望的结果。
对于新式类而言,所有的类都继承自object,所以[菱形继承]是非常普遍的现象,因此不可能采用这种MRO方式。

Python2.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]。

D.__mro__

这种MRO方式已经能偶解决【菱形继承】问题。

C3 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 方式势在必行。
为解决 Python 2.2 中 MRO 所存在的单调性和局部优先级的问题,Python 2.3以后采用了 C3 方法来确定方法解析顺序。你如果在 Python 2.3 以后版本里输入上述代码,就会产生一个异常,禁止创建具有二义性的继承关系:

class C(A,B):pass
Traceback (most recent call last):
  Python Shell, prompt 47, line 1
builtins.TypeError: Cannot create a consistent method resolution
order (MRO) for bases A, B

判断MRO要先确定一个线性序列,然后查找路径由序列中类的顺序决定。所以C3算法就是生成一个线性序列。
如果继承至一个基类:class B(A).这时B的MRO系列为[B,A]。
如果继承至多个基类:class B(A1,A2,A3…).这时B的MRO序列MRO(B)=[B]+merge(MRO(A1),MRO(A2),MRO(A3)…,[A1,A2,A3]).
merge操作就是C3算法的核心。遍历执行merge操作的序列,如果一个序列的第一个元素,是其他序列中的第一个元素,或不在其他序列出现,则从所有执行merge操作序列中删除这个元素,合并到当前的MRO中。merge操作后的序列,继续执行merge操作,直到merge操作的序列为空。如果merge操作的序列无法为空,则说明不合法。
下面进行理论分析:
我们把类C的线性比(MRO)记为L[C]=[C1,C2,…,CN]。其中C1 称为L[C]的头,其余元素[C2,…CN]称为尾。如果一个类C继承自基类B1、B2、…、BN,那么我们可以根据以下两步计算出L[C]:
 1.L[object] = [object]
 2.L[C(B1…BN)] = [C] + merge(L[B1]…L[BN],[B1]…[BN])
这里的关键在于merge,其输入是一组列表,按照如下方式输出一个列表:
 1.检查第一个列表的头元素(如 L[B1]的头),记作H。
 2.若H 未出现在其它列表的尾部,则将其输出,并将其从所有列表中删除,然后回到步骤1;否则,取出下一个列表的头部记作H,继续该步骤。
 3.重复上述步骤,直至列表为空或者不能再找出可以输出的元素。如果是前一种情况,则算法结束;如果是后一种情况,说明无法构建继承关系,python会抛出异常。

该方法有点类似于图的拓扑排序,但它同时还考虑了基类的出现顺序。我们用C3分析以下刚才的例子。
object,X,Y 的线性化结果比较简单:

L[object] = [object]
L[X] = [X, object]
L[Y] = [Y, object]

A的线性化计算如下:

L[A] = L[X, Y]
L[A] = [A] + merge(L[X],L[Y],[X],[Y])
=[A] + merge([X, object], [Y, object], [X] ,[Y])
=[A,X] +merge([object] ,[ Y,object],[ Y])
=[A, X, Y] +merge([object], [object])
=[A, X,Y,object]

注意,merge([object], [Y, object], [Y]) 中首先输出的是 Y 而不是 object。这是因为 object 虽然是第一个列表的头,但是它出现在了第二个列表的尾部。所以我们会跳过第一个列表,去检查第二个列表的头部,也就是 Y。Y 没有出现在其它列表的尾部,所以将其输出。
B的线性化结果为:

L[B] = [B, Y, X, object]

C的线性化结果:

L[C] = L[A,B]
L[C] = [C] +merge(L[A],L[B] ,[A],[B])
		=[C]+merge([A,X,Y,object], [B,Y, X,object], [A], [B])
		=[C,A] +merge([X,Y,object],[B,Y,X,object],[B])
		=[C,A,B] +merge([X,Y,object],[Y,X,object])

到了最后一步我们没有办法继续计算下去了:X虽然是第一个列表的头,但是它出现在了第二个列表的尾部;Y虽然是第二个列表的头,但是它出现在了第一个列表的尾部。因此,我们无法构建一个没有二义性的继承关系,只能手工去解决(比如改变B基类中X、Y的顺序)。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值