天天看点

pyhon新式类多重继承的mro顺序确定: c3算法

本文写作目的:试图以最清晰简洁的方式介绍python新式类的C3算法

python经典类和新式类:

 python中的新式类是指直接或间接继承自object的类,而经典类则是没有直接或间接继承object的类,新式类和经典类的重要区别就是它们的MRO(方法解析顺序)不同,经典类的mro顺序是用深度优先+从左至右的方法确定的(这里的"从左至右"是指定义子类时,多个基类的书写顺序从左至右),而新式类的MRO顺序则是用C3算法确定的(2.3版本不是C3,之后都是C3)。在python3中只支持新式类,即所有类都直接或者简介继承自object类,并且,定义一个类时如果没有显示写出基类,则默认继承自object类;在python2(2.3及以上)中则有经典类和新式类两种方式,在python2中定义新式类必须显示写出继承object类。

新式类的C3算法:

 方法解析顺序MRO其实就是确定子类和祖先类之间的一个线性顺序,这个顺序表达了各个祖先类(包括父类)对于子类的重要性(优先级),当类对象调用某个方法时,解释器会根据MRO列表去挨个找,直到找到一个同名的方法然后调用之,所以理解python继承结构中各个类的优先顺序对于写代码是必须的,C3算法过程如下。

 假设子类C的N个直接父类是B1,B2,…,BN,记L[C]是C的线性化列表(MRO表),L[B1],L[B2],…,L[BN]是N个父类的线性化列表,F=[B1,…,BN]是由N个父类构成的单个列表,其中B1,…,BN是按照定义类C时的书写顺序从左至右的。则L[C]就是由N+1个列表生成的,具体就是用下面的递归合并算法:

L[C] = [C] + merge(L[B1], L[B2] ,…, L[BN], F)

L[object] = object

其中的merge的输入是N+1个列表,按照如下方式输出一个长度为N的列表:

  1. 检查第一个列表的头元素(如 L[B1] 的头),记作 H。
  2. 若 H 未出现在其它列表的中,或者出现了但也都是在头部,则将其输出,并将其从所有列表中删除,然后回到步骤1,否则,取出下一个列表的头部记作 H,继续该步骤。重复上述步骤,直至列表为空或者不能再找出可以输出的元素。如果是前一种情况,则算法结束;如果是后一种情况,说明无法构建继承关系,Python 会抛出异常。

可能第一次看这个过程不是很能理解,举个栗子就很清晰啦。(图是偷来的,嘻嘻),需要注意一下,图中多个父类的左右顺序和继承顺序是一致的。

pyhon新式类多重继承的mro顺序确定: c3算法

如上图的继承结构,现在要求类A的MRO列表,记O为object,假设已经求出各个祖先类的MRO表如下:

L[D] = [D,O]
L[E] = [E,O]
L[F] = [F,O]
L[B] = [B,D,E,O]
L[C] = [C,D,F,O]
           

则L[A]的合并过程如下:

L[A] = A + merge([B,D,E,O],[C,D,F,O],[B,C]) # 取出B 
	 = [A,B] + merge([D,E,O],[C,D,F,O],[C]) # 取出C,因为第2个列表中D不是第1个元素
	 = [A,B,C] + merge([D,E,O],[D,F,O]) 	# 取出D
	 = [A,B,C,D] + merge([E,O],[F,O])		# 取出E
	 = [A,B,C,D,E] + merge([O],[F,O])		# 取出F
	 = [A,B,C,D,E,F] + merge([O],[O])		# 取出O
	 = [A,B,C,D,E,F,O]
           
# 下面是验证代码(python3中测试结果)
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()
#[__main__.A, __main__.B, __main__.C, __main__.D, __main__.E, main__.F, object]
           

下面再举个有冲突的栗子,继承结构如下图:

pyhon新式类多重继承的mro顺序确定: c3算法
L[X] = [X,O]
L[Y] = [Y,O]
L[A] = [A,X,Y,O]
L[B] = [B,Y,X,O]

L[C] = [C] + merge([A,X,Y,O],[B,Y,X,O],[A,B])
	 = [C,A] + merge([X,Y,O],[B,Y,X,O],[B])
	 = [C,A,B] + merge([X,Y,O],[Y,X,O]) # 此处X和Y冲突了,所以解释器会抛出异常
           
# 验证代码
class X(object): pass
class Y(object): pass
class A(X, Y): pass
class B(Y, X): pass
class C(A, B): pass
           

结果如下,抛出异常了

pyhon新式类多重继承的mro顺序确定: c3算法

几个说明:

 python新式类的C3算法能够保证以下几点:

  1. 线性单调性,是说如果一个类的MRO顺序在它任意子(孙)类MRO列表中的相对顺序是不变的,也就是说新定义类的MRO顺序必须符合原有类的MRO顺序。

  2. 局部优先级,是指几个直接父类的在定义子类时的书写顺序的相对顺序在子类中MRO中是保持一致的。(局部优先级的含义我猜的是这个意思,不一定准哟,但是后面那句话是对的)。 

  3. MRO列表中父类永远在子类后面。

 所以再计算一个继承结构的MRO列表后,可以用这几条去检查一下,如果发现有不符合的,那这个继承结构肯定是有问题的。

最后 :下面有两篇参考文献很棒,第一篇也是讲c3算法的,还讲了之前的mro算法为什么不行,第二篇是c3算法的官方文档。第一篇将c3算法时有一个错误的地方,就是他将父类的列表[B1,…,BN]写成是N个单元素的列表了,即[B1],…,B[N],其实它们是构成一个列表[B1,…,BN],按照他那个说法,下面的例子应该是对的,但实际上抛出异常了。

class A(object):pass
class B(A):pass
class C(A,B):pass 	# class C(B,A):pass 是可以的

L[A] = [A,O]
L[B] = [B,A,o]
L[C] = C + merge([A,O],[B,A,O],[A],[B])
	 = [C,B] + merge([A,O],[A,O],[A])
	 = [C,B,A] + merge([O],[O])
	 = [C,B,A,O]
# 如果将父类的列表[B1,..,BN]写成N个[B1],..,B[N],像上面的例子一样,是错误的,因为此时
# 相当于没有考虑父类的相对顺序了
# 此例子当中,A是B的父类,所以A应该在B之后,但是同时C继承A,B时,A在前,所以又应该是A在B
# 前,从而矛盾了。

# 正确解法
L[C] = C + merge([A,O],[B,A,O],[A,B]) # 此步A和B就矛盾了,所以继承结构不合法
           

第一篇: https://hanjianwei.com/2013/07/25/python-mro/

第二篇: https://www.python.org/download/releases/2.3/mro/#bad-method-resolution-orders