返回介绍

建议58:理解 MRO 与多继承

发布于 2024-01-30 22:19:09 字数 3479 浏览 0 评论 0 收藏 0

跟其他编程语言一样,Python也支持多继承。多继承的语法非常简单。

class DerivedClassName(Base1, Base2, Base3)

谈到多继承,我们来讨论一下图6-3所示的菱形继承的经典问题。

图6-3 多继承UML示意图

假设有图6-3所示继承关系,当用古典类实现的时候,如果有实例d=D(),当调用d.getvalue()和d.show()方法的时候分别对应哪个父类中的方法?当改为新式类来实现时,结果又将是怎样的呢?我们来看具体实现:

class A():
     def getvalue(self):
         print "return value of A"
     def show(self):
         print "I can show the information of A"
class B(A):
     def getvalue(self):
         print "return value of B"
class C(A):
     def getvalue(self):
         print "return value of C"
     def show(self):
         print "I can show the information of C"
class D(B,C):pass

当用古典类实现的时候我们会发现,分别调用的是B类的getvalue()方法和A类中的show()方法,而当改为新式类实现(请读者自行验证)的时候,结果却变为调用B类的getvalue()方法和C类的show()方法。从两种不同实现方式的输出上也可以证实这一点。

古典类输出如下:

     return value of B
I can show the information of A

新式类输出如下:

     return value of B
I can show the information of C

为什么两种情况下输出结果会有所不同呢?这背后到底发生了什么?根本原因在哪里?实际上,导致这些不同点的根本原因在于古典类和新式类之间所采取的MRO(Method Resolution Order,方法解析顺序)的实现方式存在差异。

在古典类中,MRO搜索采用简单的自左至右的深度优先方法,即按照多继承申明的顺序形成继承树结构,自顶向下采用深度优先的搜索顺序,当找到所需要的属性或者方法的时候就停止搜索。因此如图6-3所示,当调用d.getvalue()的时候,其搜索顺序为D->B,所以调用的是B类中对应的方法。而d.show()的搜索顺序为D->B->A,因此最后调用的是A类中对应的方法。

而新式类采用的是C3 MRO搜索方法,该算法描述如下:

假定,C1C2...CN表示类C1到CN的序列,其中序列头部元素(head)=C1,序列尾部(tail)定义为=C2..CN;

C继承的基类自左向右分别表示为B1,B2...BN;

L[C]表示C的线性继承关系,其中L[object]=object。

算法具体过程如下:

L[C(B1 ... BN)] = C + merge(L[B1] ... L[BN], B1 ... BN)

其中merge方法的计算规则如下:在L[B1]...L[BN],B1...BN中,取L[B1]的head,如果该元素不在L[B2]...L[BN],B1...BN的尾部序列中,则添加该元素到C的线性继承序列中,同时将该元素从所有列表中删除(该头元素也叫good head),否则取L[B2]的head。继续相同的判断,直到整个列表为空或者没有办法找到任何符合要求的头元素(此时将引发一个异常)。

我们结合上面的例子来说明C3 MRO算法的具体计算方法,以新式类实现的上述菱形继承关系如图6-4所示。

根据算法规则有如下关系表达式:

L(O)=O
;L(A)=AO
;

则:

L(B)=B+merge(L(A))=B+merge(AO)=B+A+merge(O,O)=B,A,O
L(C)=C+merge(L(A))=C+merge(AO)=C+A+merge(O,O)=C,A,O
L(D)=D+merge(L(B),L(C),BC)
  =D+merge(BAO,CAO,BC)
  =D+B+merge(AO,CAO,C)(
下一个计算取AO
的头A
,但A
包含在CAO
的尾部,因此不满足条件,
跳到下一个元素CAO
继续计算)
  =D+B+C+merge(AO,AO)
  =D+B+C+A+O =DBCAO

因此对于上述例子,当D的实例d调用getvalue()和show()方法时按照D->B->C->A->O的顺序进行搜索,在第一个找个该方法的位置停止搜素并调用该类对应的方法,因此getvalue()会在B的类中找到对应的方法,而show会在C()类中找到对应的方法。

关于MRO的搜索顺序我们也可以在新式类中通过查看__mro__属性得到证实。D.__mro__的输出如下:

(<class'__main__.D'>,<class'__main__.B'>,<class'__main__.C'>,<class'__mai
n__.A'>,<type'object'>)

实际上MRO虽然叫方法解析顺序,但它不仅是针对方法搜索,对于类中的数据属性也适用。读者可以自行验证。

根据C3 MRO算法的描述,如果找不到满足条件的good head,则会摒弃该元素从而对下一个元素进行查找。但如果找遍了所有的元素都找不到符合条件的good head会怎么样呢?来看一个具体例子。

class A(object): pass
class B(object): pass
class C(A, B): pass   ... ... ... ...
①基类顺序为A
,B
class D(B, A): pass   ... ... ... ...
②基类顺序为B
,A
class E(C, D): pass

运行程序我们会发现这种情况下有异常抛出。

TypeError: Error when calling the metaclass bases
   Cannot create a consistent method resolution
order (MRO) for bases B, A

根据上述代码的继承关系图(请读者自行画出)和MRO算法可以得出:

L(E)=E+merge(L(C),L(D),CD)
  =E+merge(CABO,CBAO,CD)
  =E+C+merge(ABO,BAO,D)
  =E+C+D+merge(ABO+BAO)

当算法进行到最后一步的时候便再也找不到满足条件的head了,因为当选择ABO的头A元素的时候,发现其包含在BAO的尾部AO中;同理,B包含在BO中,此时便形成了一个死锁,Python解释器此时不知道如何处理这种情况,便直接抛出异常,这就是上述例子有异常抛出的原因。

菱形继承是我们在多继承设计的时候需要尽量避免的一个问题。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文