天天看點

你真的了解Python中MRO算法嗎?

【前言】

MRO(Method Resolution Order):方法解析順序。

Python語言包含了很多優秀的特性,其中多重繼承就是其中之一,但是多重繼承會引發很多問題,比如二義性,Python中一切皆引用,這使得他不會像C++一樣使用虛基類處理基類對象重複的問題,但是如果父類存在同名函數的時候還是會産生二義性,Python中處理這種問題的方法就是MRO。

【曆史中的MRO】

如果不想了解曆史,隻想知道現在的MRO可以直接看最後的C3算法,不過C3所解決的問題都是曆史遺留問題,了解問題,才能解決問題,建議先看曆史中MRO的演化。

Python2.2以前的版本:經典類(classic class)時代

經典類是一種沒有繼承的類,執行個體類型都是type類型,如果經典類被作為父類,子類調用父類的構造函數時會出錯。

這時MRO的方法為DFS(深度優先搜尋(子節點順序:從左到右))。

Python

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

inspect.getmro(A)可以檢視經典類的MRO順序

Python

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順序如下圖:

你真的了解Python中MRO算法嗎?

兩種繼承模式在DFS下的優缺點。

第一種,我稱為正常繼承模式,兩個互不相關的類的多繼承,這種情況DFS順序正常,不會引起任何問題;

第二種,棱形繼承模式,存在公共父類(D)的多繼承(有種D字一族的感覺),這種情況下DFS必定經過公共父類(D),這時候想想,如果這個公共父類(D)有一些初始化屬性或者方法,但是子類(C)又重寫了這些屬性或者方法,那麼按照DFS順序必定是會先找到D的屬性或方法,那麼C的屬性或者方法将永遠通路不到,導緻C隻能繼承無法重寫(override)。這也就是為什麼新式類不使用DFS的原因,因為他們都有一個公共的祖先object。

Python2.2版本:新式類(new-style class)誕生

為了使類和内置類型更加統一,引入了新式類。新式類的每個類都繼承于一個基類,可以是自定義類或者其它類,預設承于object。子類可以調用父類的構造函數。

這時有兩種MRO的方法

1. 如果是經典類MRO為DFS(深度優先搜尋(子節點順序:從左到右))。

2. 如果是新式類MRO為BFS(廣度優先搜尋(子節點順序:從左到右))。

Python

1 2 3 Class A ( object ) :    # 繼承于object      def __init__ ( self ) :          print "這是新式類"

Python

1 A . __mro_ _ 可以檢視新式類的順序

MRO的BFS順序如下圖:

你真的了解Python中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算法具體實作稍後講解。

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

你真的了解Python中MRO算法嗎?

Python3到至今:新式類一統江湖

Python3開始就隻存在新式類了,采用的MRO也依舊是C3算法。

【神奇的算法C3】

C3算法解決了單調性問題和隻能繼承無法重寫問題,在很多技術文章包括官網中的C3算法,都隻有那個merge list的公式法,想看的話網上很多,自己可以查。但是從公式很難了解到解決這個問題的本質。我經過一番思考後,我講講我所了解的C3算法的本質。如果錯了,希望有人指出來。

假設繼承關系如下(官網的例子):

Python

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。

Python

1 2 3 對比一下 A . __mro_ _的結果   ( < class '__main__.A' > , < class '__main__.B' > , < class '__main__.E' > , < class