Python MRO方法解析顺序详解
我们知道,Python 类是支持(多)继承的,一个类的方法和属性可能定义在当前类,也可能定义在基类。针对这种情况,当调用类方法或类属性时,就需要对当前类以及它的基类进行搜索,以确定方法或属性的位置,而搜索的顺序就称为方法解析顺序。
方法解析顺序(Method Resolution Order),简称 MRO。对于只支持单继承的编程语言来说,MRO 很简单,就是从当前类开始,逐个搜索它的父类;而对于 Python,它支持多继承,MRO 相对会复杂一些。
实际上,Python 发展至今,经历了以下 3 种 MRO 算法,分别是:
仍以上面程序为例,通过深度优先遍历,其搜索顺序为
例如,分析如下程序(程序二):
下面来分析这样的搜索顺序是否合理,我们来看下各个类中的 MRO:
在 Python 2.3 及后续版本中,运行程序一,得到如下结果:
运行程序二,会产生如下异常:
以程序一为主,C3 把各个类的 MRO 记为如下等式:
由此,可以计算出类 B 的 MRO,其计算过程为:
方法解析顺序(Method Resolution Order),简称 MRO。对于只支持单继承的编程语言来说,MRO 很简单,就是从当前类开始,逐个搜索它的父类;而对于 Python,它支持多继承,MRO 相对会复杂一些。
实际上,Python 发展至今,经历了以下 3 种 MRO 算法,分别是:
- 从左往右,采用深度优先搜索(DFS)的算法,称为旧式类的 MRO;
- 自 Python 2.2 版本开始,新式类在采用深度优先搜索算法的基础上,对其做了优化;
- 自 Python 2.3 版本,对新式类采用了 C3 算法。由于 Python 3.x 仅支持新式类,所以该版本只使用 C3 算法。
有读者可能会好奇,为什么 MRO 弃用了前两种算法,而选择最终的 C3 算法呢?原因很简单,前 2 种算法都存在一定的问题。有关旧式类和新式类的讲解,可阅读《Python super()使用注意事项》一文。
旧式类MRO算法
在使用旧式类的 MRO 算法时,以下面代码为例(程序一):class A: def method(self): print("CommonA") class B(A): pass class C(A): def method(self): print("CommonC") class D(B, C): pass print(D().method())通过分析可以想到,此程序中的 4 个类是一个“菱形”继承的关系,当使用 D 类对象访问 method() 方法时,根据深度优先算法,搜索顺序为
D->B->A->C->A
。因此,使用旧式类的 MRO 算法最先搜索得到的是基类 A 中的 method() 方法,即在 Python 2.x 版本中,此程序的运行结果为:旧式类的 MRO 可通过使用 inspect 模块中的 getmro(类名) 函数直接获取。例如 inspect.getmro(D) 表示获取 D 类的 MRO。
CommonA
但是,这个结果显然不是想要的,我们希望搜索到的是 C 类中的 method() 方法。新式类MRO算法
为解决旧式类 MRO 算法存在的问题,Python 2.2 版本推出了新的计算新式类 MRO 的方法,它仍然采用从左至右的深度优先遍历,但是如果遍历中出现重复的类,只保留最后一个。仍以上面程序为例,通过深度优先遍历,其搜索顺序为
D->B->A->C->A
,由于此顺序中有 2 个 A,因此仅保留后一个,简化后得到最终的搜索顺序为 D->B->C->A
。可以看到,这种 MRO 方式已经能够解决“菱形”继承的问题,但是可能会违反单调性原则。所谓单调性原则,是指在类存在多继承时,子类不能改变基类的 MRO 搜索顺序,否则会导致程序发生异常。新式类可以直接通过 类名.__mro__ 的方式获取类的 MRO,也可以通过 类名.mro() 的形式,旧式类是没有 __mro__ 属性和 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
。下面来分析这样的搜索顺序是否合理,我们来看下各个类中的 MRO:
- 对于 A,其搜索顺序为 A->X->Y->object;
- 对于 B,其搜索顺序为 B->Y->X->object;
- 对于 C,其搜索顺序为 C->A->B->X->Y->object。
MRO C3
为解决 Python 2.2 中 MRO 所存在的问题,Python 2.3 采用了 C3 方法来确定方法解析顺序。多数情况下,如果某人提到 Python 中的 MRO,指的都是 C3 算法。在 Python 2.3 及后续版本中,运行程序一,得到如下结果:
CommonC
运行程序二,会产生如下异常:
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\demo.py", line 9, in <module>
class C(A, B):
TypeError: Cannot create a consistent method resolution
order (MRO) for bases X, Y
以程序一为主,C3 把各个类的 MRO 记为如下等式:
- 类 A:L[A] = merge(A , object)
- 类 B:L[B] = [B] + merge(L[A] , [A])
- 类 C:L[C] = [C] + merge(L[A] , [A])
- 类 D:L[D] = [D] + merge(L[A] , L[B] , [A] , [B])
这里的关键在于 merge,它的运算方式如下:注意,以类 A 等式为例,其中 merge 包含的 A 称为 L[A] 的头,剩余元素(这里仅有一个 object)称为尾。
- 检查第一个列表的头元素(如 L[A] 的头),记作 H。
- 若 H 未出现在 merge 中其它列表的尾部,则将其输出,并将其从所有列表中删除,然后回到步骤 1;否则,取出下一个列表的头部记作 H,继续该步骤。
由此,可以计算出类 B 的 MRO,其计算过程为:
L[B] = [B] + merge(L[A],[A]) = [B] + merge([A,object],[A]) = [B,A] + merge([object]) = [B,A,object]同理,其他类的 MRO 也可以轻松计算得出。这里不再赘述,有兴趣的读者可自行推算。
所有教程
- C语言入门
- C语言编译器
- C语言项目案例
- 数据结构
- C++
- STL
- C++11
- socket
- GCC
- GDB
- Makefile
- OpenCV
- Qt教程
- Unity 3D
- UE4
- 游戏引擎
- Python
- Python并发编程
- TensorFlow
- Django
- NumPy
- Linux
- Shell
- Java教程
- 设计模式
- Java Swing
- Servlet
- JSP教程
- Struts2
- Maven
- Spring
- Spring MVC
- Spring Boot
- Spring Cloud
- Hibernate
- Mybatis
- MySQL教程
- MySQL函数
- NoSQL
- Redis
- MongoDB
- HBase
- Go语言
- C#
- MATLAB
- JavaScript
- Bootstrap
- HTML
- CSS教程
- PHP
- 汇编语言
- TCP/IP
- vi命令
- Android教程
- 区块链
- Docker
- 大数据
- 云计算