>>> from env_helper import info; info()
页面更新时间: 2023-12-27 10:42:04
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-16-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

6.5. 理解MRO与多继承

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

class DerivecLClassName (Base1, Base2 , Base3)

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

_images/img3.png

假设有图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(seif):
>>>         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.getvalu()的时候,其搜索顺序为D->B,所以 调用的是B类中对应的方法c而d.show()的搜索顺序为D->B->A,因此最后调用的是A类 中对应的方法。

而新式类采用的是C3MRO搜索方法,该算法推述如下:

假定,C1C2…CN表示类a到CN的序列,其中序列头部元素(head)=Cl,序列尾部 (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]BL..BN中,取L[B1]的head,如果 该元素不在L[B2]...L[BN],B1...BN的尾部序列中,则添加该元素 到C的线性继承序列中,间时将该元素从所有列表中删除(该头 元索也叫good head>,否则取L[B2]的head。继续相同的判断,直 到整个列表为空或者没有办法找到任何符合要求的头元素(此时 将引发一个异常)。

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

_images/img4.png

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

L(0)=0; L(A)=A0;

则:

L(B) =B+merge(L(A))=B+merge(AO) = B+A+merg(0,0) =B, A, 0 L(C) =C+merge(L(A))=C+merge(AO) = C+A+merge (O,0) =C, A, 0 L(D) =D+merge(L (B) , L(C), BC)

=D+merge(BAO,CAO,BC)

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

=D+B+C+merge(A0,A0)

=D+B+C+A+0 =DBCA0

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

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

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

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

根据C3MRO算法的描述,如果找不到满足条件的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                                 Traceback (most recent call last)

<ipython-input-7-7271f704877d> in <module>()
      3 class C(A,B):pass  #基类顺序为A,B
      4 class D(B,A):pass #基类顺序为B,A
----> 5 class E(C,D):pass


TypeError: Cannot create a consistent method resolution
order (MRO) for bases A, B

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

根据上述代码的继承关系图(请读者自行画出)和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(AB0+BAO)

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

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