python面向对象--方法解析顺序(MRO)

2024-02-16 12:32

本文主要是介绍python面向对象--方法解析顺序(MRO),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

转载:https://www.cnblogs.com/qunxiadexiaoxiangjiao/p/8311429.html

对于支持继承的编程语言来说,其方法(属性)可能定义在当前类,也可能来自于基类,所以在方法调用时就需要对当前类和基类进行搜索以确定方法所在的位置。而搜索的顺序就是所谓的「方法解析顺序」(Method Resolution Order,或MRO)。对于只支持单继承的语言来说,MRO 一般比较简单;而对于 Python 这种支持多继承的语言来说,MRO 就复杂很多。

先看一个「菱形继承」的例子:
在这里插入图片描述

如果 x 是 D 的一个实例,那么 x.show() 到底会调用哪个 show 方法呢?如果按照 [D, B, A, C] 的搜索顺序,那么 x.show() 会调用 A.show();如果按照 [D, B, C, A] 的搜索顺序,那么 x.show() 会调用 C.show()。由此可见,MRO 是把类的继承关系线性化的一个过程,而线性化方式决定了程序运行过程中具体会调用哪个方法。既然如此,那什么样的 MRO 才是最合理的?Python 中又是如何实现的呢?

Python 至少有三种不同的 MRO:

  • 经典类(classic class)的深度遍历。
  • Python 2.2 的新式类(new-style class)预计算。
  • Python 2.3 的新式类的C3 算法。它也是 Python 3 唯一支持的方式

经典类的MRO

Python 有两种类:经典类(classic class)和新式类(new-style class)。两者的不同之处在于新式类继承自 object。在 Python 2.1 以前,经典类是唯一可用的形式;Python 2.2 引入了新式类,使得类和内置类型更加统一;在 Python 3 中,新式类是唯一支持的类。

经典类采用了一种很简单的 MRO 方法:从左至右的深度优先遍历。以上述「菱形继承」为例,其查找顺序为 [D, B, A, C, A],如果只保留重复类的第一个则结果为 [D,B,A,C]。我们可以用 inspect.getmro 来获取类的 MRO:

>>> import inspect
>>> class A:
...     def show(self):
...         print "A.show()"
...
>>> class B(A): pass
>>> class C(A):
...     def show(self):
...         print "C.show()"
...
>>> class D(B, C): pass
>>> inspect.getmro(D)
(<class __main__.D at 0x105f0a6d0>, <class __main__.B at 0x105f0a600>, <class __main__.A at 0x105f0a668>, <class __main__.C at 0x105f0a738>)
>>> x = D()
>>> x.show()
A.show()

这种深度优先遍历对于简单的情况还能处理的不错,但是对于上述「菱形继承」其结果却不尽如人意:虽然 C.show() 是 A.show() 的更具体化版本(显示了更多的信息),但我们的x.show() 没有调用它,而是调用了 A.show()。这显然不是我们希望的结果。

对于新式类而言,所有的类都继承自 object,所以「菱形继承」是非常普遍的现象,因此不可能采用这种 MRO 方式。

Python 2.2 的新式类 MRO

为解决经典类 MRO 所存在的问题,Python 2.2 针对新式类提出了一种新的 MRO 计算方式:在定义类时就计算出该类的 MRO 并将其作为类的属性。因此新式类可以直接通过__mro__属性获取类的 MRO。
Python 2.2 的新式类 MRO 计算方式和经典类 MRO 的计算方式非常相似:它仍然采用从左至右的深度优先遍历,但是如果遍历中出现重复的类,只保留最后一个。重新考虑上面「菱形继承」的例子,由于新式类继承自 object 因此类图稍有改变[新式类菱形继承]:
在这里插入图片描述

按照深度遍历,其顺序为 [D, B, A, object, C, A, object],重复类只保留最后一个,因此变为 [D, B, C, A, object]。代码为:

>>> class A(object):
...     def show(self):
...         print "A.show()"
...
>>> class B(A): pass
>>> class C(A):
...     def show(self):
...         print "C.show()"
...
>>> class D(B, C): pass
>>> D.__mro__
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <type 'object'>)
>>> x = D()
>>> x.show()
C.show()

这种 MRO 方式已经能够解决「菱形继承」问题,再让我们看个稍微复杂点的例子:

在这里插入图片描述

>>> class X(object): pass
>>> class Y(object): pass
>>> class A(X, Y): pass
>>> class B(Y, X): pass
>>> class C(A, B): pass

首先进行深度遍历,结果为 [C, A, X, object, Y, object, B, Y, object, X, object];然后,只保留重复元素的最后一个,结果为 [C, A, B, Y, X, object]。Python 2.2 在实现该方法的时候进行了调整,使其更尊重基类中类出现的顺序,其实际结果为 [C, A, B, X, Y, object]。

这样的结果是否合理呢?首先我们看下各个类中的方法解析顺序:对于 A 来说,其搜索顺序为[A, X, Y, object];对于 B,其搜索顺序为 [B, Y, X, object];对于 C,其搜索顺序为[C, A, B, X, Y, object]。我们会发现,B 和 C 中 X、Y 的搜索顺序是相反的!也就是说,当 B 被继承时,它本身的行为竟然也发生了改变,这很容易导致不易察觉的错误。此外,即使把 C 搜索顺序中 X 和 Y 互换仍然不能解决问题,这时候它又会和 A 中的搜索顺序相矛盾。

事实上,不但上述特殊情况会出现问题,在其它情况下也可能出问题。其原因在于,上述继承关系违反了线性化的「 单调性原则 」。Michele Simionato对单调性的定义为:

A MRO is monotonic when the following is true: if C1 precedes C2 in the linearization of C, then C1 precedes C2 in the linearization of any subclass of C. Otherwise, the innocuous operation of deriving a new class could change the resolution order of methods, potentially introducing very subtle bugs.

也就是说,子类不能改变基类的方法搜索顺序。在 Python 2.2 的 MRO 算法中并不能保证这种单调性,它不会阻止程序员写出上述具有二义性的继承关系,因此很可能成为错误的根源。

除了单调性之外,Python 2.2 及 经典类的 MRO 也可能违反继承的「 局部优先级 」,具体例子可以参见官方文档。采用一种更好的 MRO 方式势在必行。

C3 MRO

为解决 Python 2.2 中 MRO 所存在的问题,Python 2.3以后采用了 C3 方法来确定方法解析顺序。你如果在 Python 2.3 以后版本里输入上述代码,就会产生一个异常,禁止创建具有二义性的继承关系:

>>> class C(A, B): pass
Traceback (most recent call last):File "<ipython-input-8-01bae83dc806>", line 1, in <module>class C(A, B): pass
TypeError: Error when calling the metaclass basesCannot create a consistent method resolution
order (MRO) for bases X, Y

我们把类 C 的线性化(MRO)记为 L[C] = [C1, C2,…,CN]。其中 C1 称为 L[C] 的头,其余元素 [C2,…,CN] 称为尾。如果一个类 C 继承自基类 B1、B2、……、BN,那么我们可以根据以下两步计算出 L[C]:

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

这里的关键在于 merge,其输入是一组列表,按照如下方式输出一个列表:

  1. 检查第一个列表的头元素(如 L[B1] 的头),记作 H。
  2. 若 H 未出现在其它列表的尾部,则将其输出,并将其从所有列表中删除,然后回到步骤1;否则,取出下一个列表的头部记作 H,继续该步骤。
  3. 重复上述步骤,直至列表为空或者不能再找出可以输出的元素。如果是前一种情况,则算法结束;如果是后一种情况,说明无法构建继承关系,Python 会抛出异常。

该方法有点类似于图的拓扑排序,但它同时还考虑了基类的出现顺序。我们用 C3 分析一下刚才的例子。
object,X,Y 的线性化结果比较简单:

L[object] = [object]
L[X] = [X, object]
L[Y] = [Y, object]

A 的线性化计算如下:

L[A] = [A] + merge(L[X], L[Y], [X], [Y])= [A] + merge([X, object], [Y, object], [X], [Y])= [A, X] + merge([object], [Y, object], [Y])= [A, X, Y] + merge([object], [object])= [A, X, Y, object]

注意第3步,merge([object], [Y, object], [Y]) 中首先输出的是 Y 而不是 object。这是因为 object 虽然是第一个列表的头,但是它出现在了第二个列表的尾部。所以我们会跳过第一个列表,去检查第二个列表的头部,也就是 Y。Y 没有出现在其它列表的尾部,所以将其输出。
同理,B 的线性化结果为

L[B] = [B, Y, X, object]

最后,我们看看 C 的线性化结果:

L[C] = [C] + merge(L[A], L[B], [A], [B])= [C] + merge([A, X, Y, object], [B, Y, X, object], [A], [B])= [C, A] + merge([X, Y, object], [B, Y, X, object], [B])= [C, A, B] + merge([X, Y, object], [Y, X, object])

到了最后一步我们没有办法继续计算下去 了:X 虽然是第一个列表的头,但是它出现在了第二个列表的尾部;Y 虽然是第二个列表的头,但是它出现在了第一个列表的尾部。因此,我们无法构建一个没有二义性的继承关系,只能手工去解决(比如改变 B 基类中 X、Y 的顺序)。
我们再看一个没有冲突的例子:

在这里插入图片描述

计算过程如下:

L[object] = [object]
L[D] = [D, object]
L[E] = [E, object]
L[F] = [F, object]
L[B] = [B, D, E, object]
L[C] = [C, D, F, object]
L[A] = [A] + merge(L[B], L[C], [B], [C])= [A] + merge([B, D, E, object], [C, D, F, object], [B], [C])= [A, B] + merge([D, E, object], [C, D, F, object], [C])= [A, B, C] + merge([D, E, object], [D, F, object])= [A, B, C, D] + merge([E, object], [F, object])= [A, B, C, D, E] + merge([object], [F, object])= [A, B, C, D, E, F] + merge([object], [object])= [A, B, C, D, E, F, object]

当然,可以用代码验证类的 MRO,上面的例子可以写作:

>>> class D(object): pass
>>> class E(object): pass
>>> class F(object): pass
>>> class B(D, E): pass
>>> class C(D, F): pass
>>> class A(B, C): pass
>>> A.__mro__
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>,<class '__main__.E'>,<class '__main__.F'>)

s C(D, F): pass

class A(B, C): pass
A.mro
(<class ‘main.A’>, <class ‘main.B’>, <class ‘main.C’>, <class ‘main.D’>,<class ‘main.E’>,<class ‘main.F’>)


这篇关于python面向对象--方法解析顺序(MRO)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/714535

相关文章

python获取指定名字的程序的文件路径的两种方法

《python获取指定名字的程序的文件路径的两种方法》本文主要介绍了python获取指定名字的程序的文件路径的两种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要... 最近在做项目,需要用到给定一个程序名字就可以自动获取到这个程序在Windows系统下的绝对路径,以下

JavaScript中的高级调试方法全攻略指南

《JavaScript中的高级调试方法全攻略指南》什么是高级JavaScript调试技巧,它比console.log有何优势,如何使用断点调试定位问题,通过本文,我们将深入解答这些问题,带您从理论到实... 目录观点与案例结合观点1观点2观点3观点4观点5高级调试技巧详解实战案例断点调试:定位变量错误性能分

使用Python批量将.ncm格式的音频文件转换为.mp3格式的实战详解

《使用Python批量将.ncm格式的音频文件转换为.mp3格式的实战详解》本文详细介绍了如何使用Python通过ncmdump工具批量将.ncm音频转换为.mp3的步骤,包括安装、配置ffmpeg环... 目录1. 前言2. 安装 ncmdump3. 实现 .ncm 转 .mp34. 执行过程5. 执行结

Python实现批量CSV转Excel的高性能处理方案

《Python实现批量CSV转Excel的高性能处理方案》在日常办公中,我们经常需要将CSV格式的数据转换为Excel文件,本文将介绍一个基于Python的高性能解决方案,感兴趣的小伙伴可以跟随小编一... 目录一、场景需求二、技术方案三、核心代码四、批量处理方案五、性能优化六、使用示例完整代码七、小结一、

Python中 try / except / else / finally 异常处理方法详解

《Python中try/except/else/finally异常处理方法详解》:本文主要介绍Python中try/except/else/finally异常处理方法的相关资料,涵... 目录1. 基本结构2. 各部分的作用tryexceptelsefinally3. 执行流程总结4. 常见用法(1)多个e

Python中logging模块用法示例总结

《Python中logging模块用法示例总结》在Python中logging模块是一个强大的日志记录工具,它允许用户将程序运行期间产生的日志信息输出到控制台或者写入到文件中,:本文主要介绍Pyt... 目录前言一. 基本使用1. 五种日志等级2.  设置报告等级3. 自定义格式4. C语言风格的格式化方法

Python实现精确小数计算的完全指南

《Python实现精确小数计算的完全指南》在金融计算、科学实验和工程领域,浮点数精度问题一直是开发者面临的重大挑战,本文将深入解析Python精确小数计算技术体系,感兴趣的小伙伴可以了解一下... 目录引言:小数精度问题的核心挑战一、浮点数精度问题分析1.1 浮点数精度陷阱1.2 浮点数误差来源二、基础解决

使用Python实现Word文档的自动化对比方案

《使用Python实现Word文档的自动化对比方案》我们经常需要比较两个Word文档的版本差异,无论是合同修订、论文修改还是代码文档更新,人工比对不仅效率低下,还容易遗漏关键改动,下面通过一个实际案例... 目录引言一、使用python-docx库解析文档结构二、使用difflib进行差异比对三、高级对比方

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法

《JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法》:本文主要介绍JavaScript中比较两个数组是否有相同元素(交集)的三种常用方法,每种方法结合实例代码给大家介绍的非常... 目录引言:为什么"相等"判断如此重要?方法1:使用some()+includes()(适合小数组)方法2