MRO(Method Resolution Order):方法解析顺序。
Python语言包含了不少优秀的特性,其中多重继承就是其中之一,可是多重继承会引起不少问题,好比二义性,Python中一切皆引用,这使得他不会像C++同样使用虚基类处理基类对象重复的问题,可是若是父类存在同名函数的时候仍是会产生二义性,Python中处理这种问题的方法就是MRO。python
若是不想了解历史,只想知道如今的MRO能够直接看最后的C3算法,不过C3所解决的问题都是历史遗留问题,了解问题,才能解决问题,建议先看历史中MRO的演化。
Python2.2之前的版本:金典类(classic class)时代
金典类是一种没有继承的类,实例类型都是type类型,若是经典类被做为父类,子类调用父类的构造函数时会出错。
这时MRO的方法为DFS(深度优先搜索(子节点顺序:从左到右))。算法
1 |
Class A: # 是没有继承任何父类的 |
inspect.getmro(A)能够查看金典类的MRO顺序数据结构
1 |
import inspect |
MRO的DFS顺序以下图:ide
两种继承模式在DFS下的优缺点。
第一种,我称为正常继承模式,两个互不相关的类的多继承,这种状况DFS顺序正常,不会引发任何问题;函数第二种,棱形继承模式,存在公共父类(D)的多继承(有种D字一族的感受),这种状况下DFS一定通过公共父类(D),这时候想一想,若是这个公共父类(D)有一些初始化属性或者方法,可是子类(C)又重写了这些属性或者方法,那么按照DFS顺序一定是会先找到D的属性或方法,那么C的属性或者方法将永远访问不到,致使C只能继承没法重写(override)。这也就是为何新式类不使用DFS的缘由,由于他们都有一个公共的祖先object。spa
Python2.2版本:新式类(new-style class)诞生
为了使类和内置类型更加统一,引入了新式类。新式类的每一个类都继承于一个基类,能够是自定义类或者其它类,默认承于object。子类能够调用父类的构造函数。code这时有两种MRO的方法
1. 若是是金典类MRO为DFS(深度优先搜索(子节点顺序:从左到右))。
2. 若是是新式类MRO为BFS(广度优先搜索(子节点顺序:从左到右))。对象
1 |
Class A(object): # 继承于object |
1 |
A.__mro__ 能够查看新式类的顺序 |
两种继承模式在BFS下的优缺点。
第一种,正常继承模式,看起来正常,不过实际上感受很别扭,好比B明明继承了D的某个属性(假设为foo),C中也实现了这个属性foo,那么BFS明明先访问B而后再去访问C,可是为何foo这个属性会是C?这种应该先从B和B的父类开始找的顺序,咱们称之为单调性。继承第二种,棱形继承模式,这种模式下面,BFS的查找顺序虽然解了DFS顺序下面的棱形问题,可是它也是违背了查找的单调性。
由于违背了单调性,因此BFS方法只在Python2.2中出现了,在其后版本中用C3算法取代了BFS。
Python2.3到Python2.7:金典类、新式类和平发展
由于以前的BFS存在较大的问题,因此从Python2.3开始新式类的MRO取而代之的是C3算法,咱们能够知道C3算法确定解决了单调性问题,和只能继承没法重写的问题。C3算法具体实现稍后讲解。
Python3到至今:新式类一统江湖
Python3开始就只存在新式类了,采用的MRO也依旧是C3算法。
C3算法解决了单调性问题和只能继承没法重写问题,在不少技术文章包括官网中的C3算法,都只有那个merge list的公式法,想看的话网上不少,本身能够查。可是从公式很难理解到解决这个问题的本质。我通过一番思考后,我讲讲我所理解的C3算法的本质。若是错了,但愿有人指出来。
假设继承关系以下(官网的例子):
1 |
class D(object): |
首先假设继承关系是一张图(事实上也是),咱们按类继承是的顺序(class A(B, C)括号里面的顺序B,C),子类指向父类,构一张图。
咱们要解决两个问题:单调性问题和不能重写的问题。
很容易发现要解决单调性,只要保证从根(A)到叶(object),从左到右的访问顺序便可。
那么对于只能继承,不能重写的问题呢?先分析这个问题的本质缘由,主要是由于先访问了子类的父类致使的。那么怎么解决只能先访问子类再访问父类的问题呢?若是熟悉图论的人应该能立刻想到拓扑排序,这里引用一下百科的的定义:对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中全部顶点排成一个线性序列,使得图中任意一对顶点u和v,若边(u,v)∈E(G),则u在线性序列中出如今v以前。一般,这样的线性序列称为知足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序获得该集合上的一个全序,这个操做称之为拓扑排序。
由于拓扑排序确定是根到叶(也不能说是叶了,由于已经不是树了),因此只要知足从左到右,获得的拓扑排序就是结果,关于拓扑排序算法,大学的数据结构有教,这里不作讲解,不懂的能够自行谷歌或者翻一下书,建议了解完算法再往下看。
那么模拟一下例子的拓扑排序:首先找入度为0的点,只有一个A,把A拿出来,把A相关的边剪掉,再找下一个入度为0的点,有两个点(B,C),取最左原则,拿B,这是排序是AB,而后剪B相关的边,这时候入度为0的点有E和C,取最左。这时候排序为ABE,接着剪E相关的边,这时只有一个点入度为0,那就是C,取C,顺序为ABEC。剪C的边获得两个入度为0的点(DF),取最左D,顺序为ABECD,而后剪D相关的边,那么下一个入度为0的就是F,而后是object。那么最后的排序就为ABECDFobject。
1 |
对比一下 A.__mro__的结果 |
彻底正确!
本应该就这里完了,可是后期一些细心的读者仍是发现了问题。以上算法并不彻底正确。感谢 @Tiger要好好写论文 指出。
下面咱们来看看这个问题:Tiger指出了两点,一点是图中左右顺序比较难区分,还有一点是某种不可序列化的状况下,个人算法会有一些问题,针对这两点我作了改进。
先来看看出错的状况:
1 |
class A(object): |
若是使用原来的算法,咱们搞不清楚A和B谁在左边谁在右边,因此会选择其中之一,继续拓扑下去,其实这里已是有歧义了不可以解析出正确的顺序,应该报错,这使我从新思考了左右的问题。
咱们能够发现其中左右问题无非出如今两种状况,第一种状况是:图中E先继承C,再继承D;第二种状况是:先继承C的基类,再去继承D。针对这两种状况给出的方案就是图中添加的橙色的边,表示的是第一种状况的顺序问题,好比C->D,就是表示E(C,D>中的继承顺序。
那么第二种状况怎么保证先C的基类,而后再考虑D呢。咱们能够这么作,若是出现多个入度为0的点,咱们先找是刚刚剪出来的点的基类的点。这里能够看以前官网的那个例子,在E点和C点选择的时候,由于E是B的基类点,因此先选它,其实这也很容易实现,只须要记录下每一个节点的子类点(可能有多个)。
那么左右的问题也就解决了。
原文地址:http://xymlife.com/2016/05/22/python_mro/