21. Python的方法解析顺序

Python的方法解析顺序

原文by Guido van Rossum 2010-6-23 翻译by kant


一、 问题

在支持多重继承的编程语言中,从父类中查找方法的顺序一般称做方法解析顺序(Method Resolution Order),或者 MRO。(在 Python 中,查找其它属性也遵循同一顺序)对只支持单一继承的语言来说,MRO 算法没什么好说的,但如果要支持多重继承,这个算法就会变得异常复杂。

Python 至少用过三种不同的 MRO 算法:经典类所用的算法,Python 2.2 中的新式类所用算法,以及 Python 2.3 中的新式类所用的算法(也称 C3算法),而在 Python 3 中,只留下了最后一种算法。

经典类中的 MRO 算法非常简单:深度优先,从左至右,返回找到的第一个结果。例如,有如下类:


    ''class A:
    '' def save(self): pass
    ''class B(A): pass
    ''class C:
    '' def save(self): pass
    ''class D(B C): pass

如果我们创建一个类 D 的实例,其方法解析顺序为:类 D,B,A,C。因此,调用 x.save() 是实际调用的是 A.save()(而不是 C.save())。在比较的简单场景中,这种顺序是没问题的,但在复杂的多重继承中,就会遇到许多问题。其中一个就是“菱形继承(diamond inheritance)”问题,例如:


    ''class A:
    '' def save(self): pass
    ''class B(A): pass
    ''class C(A):
    '' def save(self): pass
    ''class D(B C): pass

类 D 继承自类 B 和类 C,而它们都继承自类 A。根据上面的方法解析顺序,查找顺序依次是类 D,B,A,C,A,即和之前一样, x.save() 会调用 A.save()。然而,这个结果与通常的用户预期并不一致,因为类 B 和类 C 都继承自类 A,那么,在类 C 中重载的 C.save() 似乎要更比 A.save() 更为“特殊化”,因而也要有更优先的顺序。比如,如果用户希望通过 save() 方法保存对象状态,那么不调用 C.save() 就很可能忽略掉类 C 的状态。

“菱形继承”在经典类的代码中并不常见,但新式类引入后,就会变得无处不在。因为所有新式类的最终父类都是 object 类。比如说:


    ''class B(object): pass
    ''class C(object):
    '' def __setattr__(self name value): pass
    ''class D(BC): pass

而且,object 类定义了一系列可由子类进行扩展的方法(如 setattr()),因此,方法解析顺序就变得更为关键了。在上面的例子中,类 D 的实例显然更应该调用 C.setattr


二、引入新算法

为解决这个问题,我在 Python 2.2 引入了一种新的 MRO 算法,在定义类时预先计算其方法解析顺序,并作为类的属性存储下来。在文档中,计算的逻辑依然被解释为深度优先,从左至右,但当某个类存在重复时,以最后出现的那个位置为准。于是,在上面的例子中,方法解析顺序就变为类 D,B,C,A(之前是 D,B,A,C,A,因为 A 重复,只保留了最后一个)。

当然,在实践中,问题要更为复杂。我发现有几种情况是这种算法无法解决的,因此,又处理了一些特例:两个父类被两个不同的类以不同的顺序继承,而这两个不同的类又被同一个类继承。例如:


    ''class A(object): pass
    ''class B(object): pass
    ''class X(A B): pass
    ''class Y(B A): pass
    ''class Z(XY): pass

如果使用上面的新算法,方法解析顺序是类 Z,X,Y,B,A,object。然而,我认为 B 不应该在 A 前面。于是,在真正的实现中,解析顺序的结果为类 Z,X,Y,A,B,object。从直觉看,这个算法的查找顺序试图保持父类的出现顺序。比如说,在类 Z 中,会先查找父类 X,因为它是最先出现的。

在这里,实际实现和文档记录是不一致的(而且我天真地认为这个问题影响不大)。


三、引入C3算法

Python 2.2 发布后,很快,Samuele Pedroni 就发现官方文档所描述的 MRO 和实际结果不一致,而且除了之前所描述的特例,其它情况下也出现了不一致。经过大量讨论,我们认为 Python 2.2 中引入的 MRO 是不成立的,并决定引入论文“A Monotonic Superclass Linearization for Dylan”所介绍的 C3 线性算法。

python 2.2 中引入的 MRO 的主要问题是没有实现单调性。在复杂的继承层级关系中,每一次继承都会产生一个对应的父类解析顺序。如果类 A 继承了类 B,显然,MRO 应该先查找类 A,再查找类 B,同样,如果类 B 继承类 C 和类 D,那么应该先查找类 B,之后是类 C,最后才是类 D。

用户需要在复杂的继承层级关系中完全遵循单调性原则。也就是说,如果在某次继承中,已经让类 A 的查找顺序先于类 B,就不应该在另一次继承关系中出现类 B 的查找顺序先于类 A 的情况(否则就会出现未定义的情形,这个继承关系就应该被拒绝)。这正是原来的 MRO 没做到而 C3 算法可以做到的地方。

简单地说,C3 算法的基本逻辑是,我们定义好每次继承的关系顺序,而算法会将所有顺序以一种满足单调性的方式整合起来,如果整合过程出现冲突,算法就会返回错误。

这样,在 Python 2.3 中,我们引入了学院派的 C3 算法,抛弃了我自己想象出来的 MRO 算法。其中一个结果就是,如果用户的继承关系不能保持一致,Python 会抛出异常。

比如,在之前的例子中,类 X 和类 Y 的继承顺序就存在冲突。对类 X 而言,类 A 的查找顺序先于类 B;而对类 Y 来说,类 B 的查找顺序又优先于类 A。如果这两个继承关系是相互独立的,也没有问题,但要在某次继承关系中通过继承类 X 和类 Y(比如例子中的类 Z),C3 算法就会抛出异常。

当然,这正与“Python 之禅”中的原则相符:错误不应被忽略。"


发表评论

评论列表,共 0 条评论