今天我想对Python中的方法解析顺序使用的C3算法进行一个梳理,文章内容主要基于对The Python 2.3 Method Resolution Order一文的节选翻译。
首先,需要明白的是C3算法工作于Python 2.2引入的新式类(new style classes),经典类(classic classes)中方法的解析仍然保持他们原有的顺序,即深度优先,从左至右,在此不进行深一步的讨论。
先来看一个例子:
继承顺序如图:
在这种情况下,通过A和B派生出一个新类C是有问题的,因为在A的继承中X先于Y,而在B中Y先于X,因此C中方法解析顺序会存在歧义。Python 2.3在这种情况下通过抛出异常TypeError: MRO conflict among bases Y, X来避免程序员创建有歧义的类。
C3算法
首先介绍一些简易记号来方便接下来的描述。
表示一个类的列表[C1, C2, ..., Cn]。
列表的head为其第一个元素, tail为其余下元素:
使用
来表示[C]+[C1, C2, ..., Cn]。
线性化(linearization)定义:
用符号记号来描述:
特别的,如果C是object类,即不存在父类,其线性化结果为:
然而,要计算合并顺序需要遵循以下规则:
下面举例说明,考虑如下继承:
继承顺序如图:
B的线性化计算公式可以表示如下:
根据前述规则,我们首先取D作为head,待合并列表变成了merge(O,EO,E),由于O不符合条件,我们跳过第一个列表在第二个列表中选择符合条件的E作为head,待合并列表变成了merge(O,O),最后我们选择了O,因此:
同理可以得到C的线性化结果:
最后来计算A的线性化结果:
在Python 2.2以后,可以直接通过调用.mro()方法获得MRO:
>>>A.mro()[<class'__main__.A'>,<class'__main__.B'>,<class'__main__.E'>,<class'__main__.C'>,<class'__main__.D'>,<class'__main__.F'>,<type'object'>]
最后,让我们回到最初的那个例子,其所有类的线性化结果计算如下:
然而,对于继承自类A和类B的类C来说是无法线性化的:
在此刻,我们无法完成对XYO和YXO的合并,因为X是YXO的尾,同时Y是XYO的尾,因此C3算法停止,Python 2.3将会抛出异常并拒绝创建类C。
不好的MRO
当一个MRO破坏了局部优先顺序和单调性等基础性质时称其为不好的MRO。
考虑如下例子:
创建了F、E、G三个类,其中类E可表示为class E(F),类G可表示为class G(F, E),我们期望类G的remember2buy属性是继承自F而不是E的,然而在Python 2.2中我们会得到
这破坏了局部优先顺序,因为对于类G的继承顺序(F, E),其局部优先顺序并没有在Python 2.2线性化结果中得到保留:
有人可能会争辩说Python 2.2线性化结果中类F在类E之后的原因是因为类F没有类E更具体,因为类F是类E的父类;尽管如此打破了局部优先顺序会使得代码不直观且容易出错,一个有力的佐证就是其与经典类的不同:
回想之前谈到的,经典类的继承顺序为深度优先,从左至右,因此类G的MRO为GFEF,在这种情况下局部优先顺序得到了保留。
简而言之,像之前那种继承方式应该避免,Python 2.3开始通过抛出异常来避免了这种歧义,有力地阻止了程序员来创建有歧义的类继承(通过C3算法失败来完成)。
还有一点相关的,Python 2.3的算法足够智能来发现一些显而易见的错误,比如重复继承同一个父类:
而这种情况在Python 2.2中(无论是经典类还是新式类),都不会抛出异常。
最后,有一点十分重要的需要记住:
MRO在决定方法解析顺序同时也决定了属性的解析顺序
讨论完了局部优先顺序,下面再来看单调性问题。要证明经典类的MRO是非单调的很容易:
另一方面,Python 2.2和Python 2.3的MRO中都不存在问题,都给出了:
Guido在他的一篇文章指出了经典类的MRO在实际使用中其实并不差,因为它可以使经典类避免钻石型结构。但由于所有新式类都继承自object,因此钻石型结果无法避免而且在所有的多继承图中前后矛盾都会出现。
Python 2.2的MRO使得破坏单调性十分困难,但并非不可能。接下来这个由Samuele Pedroni提供的例子,表明了Python 2.2新式类的MRO是非单调的:
使用C3算法的线性化结果如下:
Python 2.2对于A、B、C、D、E、K1、K2、K3的线性化给出了相同结果,但是对于Z则不同:
显然这个线性化结果是错的,因为类先于D出现了,而K3的线性化结果中D先于A出现。换句话说,在K3中,从类A继承的方法被从类D继承的方法覆盖了,但是在Z中,作为一个K3的子类,却使用了从类A继承的方法去覆盖从类D继承的方法!这违反了单调性。此外,Python 2.2中类Z的线性化结果与局部优先顺序也不一致,类Z的局部优先列表为[K1, K2, K3](K2在K3之前),而在线性化结果中K3在K2之前,这些问题解释了为什么2.2中的规则被弃用转投C3规则。
super函数
最后再来看Python 2.2引入的super函数,它主要用于初始化父类,避免了直接调用父类的__init__函数,减少耦合性,来看以下代码:
它的计算顺序为5 * (5 + 2)而不是(5 * 5) + 2,原因在于程序的运行顺序与类GoodWay的MRO保持了一致,通过查看GoodWay.mro()可以得到:
>>>GoodWay.mro()[<class'__main__.GoodWay'>,<class'__main__.TimesFive'>,<class'__main__.PlusTwo'>,<class'__main__.Base'>,<class'object'>]
调用GoodWay(5)的时候,它会调用TimesFive.__init__,而TimesFive.__init__又会调用PlusTwo.__init__,而PlusTwo.__init__会调用Base.__init__,当到达了钻石体系的顶部之后,所有的初始化方法会按照与刚才那些__init__相反的顺序执行,因此Base.__init__将value设为5,PlusTwo.__init__在此基础上加2,value变为7,最后TimesFive.__init__将value乘以5,得到35。
特别注意
一看到super这个函数很多人第一想法就是父类,但其实super工作原理是这样的:
根据实例inst获得其类的MRO列表,返回cls所在位置的下一个位置的类,其中inst永远是最开始那个实例。
Python 3提供了一种不带参数的super调用方式,例如:
由于Python 3程序可以在方法中通过class变量准确地引用当前类,所以上面的这种写法能够正常运作,而Python 2中则没有定义class,故而不能采用这种写法。可能有人试着用self.class做参数来调用super,但实际上这么做不行,因为Python 2是用特殊方式实现super的。
本文原文地址:http://youchen.me/2017/01/22/Python-Method%20Resolution%20Order/