你真的理解Python中MRO算法嗎?[轉]

【前言】

MRO(Method Resolution Order):方法解析順序
Python語言包含了不少優秀的特性,其中多重繼承就是其中之一,可是多重繼承會引起不少問題,好比二義性,Python中一切皆引用,這使得他不會像C++同樣使用虛基類處理基類對象重複的問題,可是若是父類存在同名函數的時候仍是會產生二義性,Python中處理這種問題的方法就是MRO。python

【歷史中的MRO】

若是不想了解歷史,只想知道如今的MRO能夠直接看最後的C3算法,不過C3所解決的問題都是歷史遺留問題,瞭解問題,才能解決問題,建議先看歷史中MRO的演化。
Python2.2之前的版本:金典類(classic class)時代
金典類是一種沒有繼承的類,實例類型都是type類型,若是經典類被做爲父類,子類調用父類的構造函數時會出錯。
這時MRO的方法爲DFS(深度優先搜索(子節點順序:從左到右))。算法

1
2
3
Class A:   # 是沒有繼承任何父類的
def __init__(self):
print "這是金典類"

inspect.getmro(A)能夠查看金典類的MRO順序數據結構

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import inspect
class D:
pass

class C(D):
pass

class B(D):
pass

class A(B, C):
pass

if __name__ == '__main__':
print inspect.getmro(A)

>> (<class __main__.A at 0x10e0e5530>, <class __main__.B at 0x10e0e54c8>, <class __main__.D at 0x10e0e53f8>, <class __main__.C at 0x10e0e5460>)

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
2
3
Class A(object):   # 繼承於object
def __init__(self):
print "這是新式類"
1
A.__mro__ 能夠查看新式類的順序

MRO的BFS順序以下圖:
排序

兩種繼承模式在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算法具體實現稍後講解。

MRO的C3算法順序以下圖:看起簡直是DFS和BFS的合體有木有。可是僅僅是看起來像而已。


Python3到至今:新式類一統江湖
Python3開始就只存在新式類了,採用的MRO也依舊是C3算法。

【神奇的算法C3】

C3算法解決了單調性問題和只能繼承沒法重寫問題,在不少技術文章包括官網中的C3算法,都只有那個merge list的公式法,想看的話網上不少,本身能夠查。可是從公式很難理解到解決這個問題的本質。我通過一番思考後,我講講我所理解的C3算法的本質。若是錯了,但願有人指出來。

假設繼承關係以下(官網的例子):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class D(object):
pass

class E(object):
pass

class F(object):
pass

class C(D, F):
pass

class B(E, D):
pass

class A(B, C):
pass

if __name__ == '__main__':
print A.__mro__

首先假設繼承關係是一張圖(事實上也是),咱們按類繼承是的順序(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
2
3
對比一下 A.__mro__的結果

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

徹底正確!
本應該就這裏完了,可是後期一些細心的讀者仍是發現了問題。以上算法並不徹底正確。感謝 @Tiger要好好寫論文 指出。
下面咱們來看看這個問題:Tiger指出了兩點,一點是圖中左右順序比較難區分,還有一點是某種不可序列化的狀況下,個人算法會有一些問題,針對這兩點我作了改進。
先來看看出錯的狀況:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A(object):
pass

class B(object):
pass

class C(A, B):
pass

class D(B, A):
pass

class E(C, D):
pass

構成對應的圖,以下其中橙色的線是改進的地方。

若是使用原來的算法,咱們搞不清楚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/

相關文章
相關標籤/搜索