>>> 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所示的菱形继承的经典问题。
假设有图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所示。
根据算法规则冇如下关系表达式:
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解释器此时不知道如何处理这种情况,便直接抛出异常.这就是上述例子有异常抛出的原因。
菱形继承是我们在多继承设计的时候需要尽量避免的一个问题。