Python的方法解析順序(MRO)[轉]

本文轉載自: http://hanjianwei.com/2013/07/25/python-mro/python

對於支持繼承的編程語言來講,其方法(屬性)可能定義在當前類,也可能來自於基類,因此在方法調用時就須要對當前類和基類進行搜索以肯定方法所在的位置。而搜索的順序就是所謂的「方法解析順序」(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 中,新式類是惟一支持的類。3d

經典類採用了一種很簡單的 MRO 方法:從左至右的深度優先遍歷。以上述「菱形繼承」爲例,其查找順序爲 [D, B, A, C, A],若是隻保留重複類的第一個則結果爲 [D,B,A,C]。咱們能夠用 inspect.getmro 來獲取類的 MRO:code

>>> 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()。這顯然不是咱們但願的結果。blog

對於新式類而言,全部的類都繼承自 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 bases
    Cannot create a consistent method resolution
order (MRO) for bases X, Y

咱們把類 C 的線性化(MRO)記爲 L[C] = [C1, C2,…,CN]。其中 C1 稱爲 L[C] 的頭,其他元素 [C2,…,CN] 稱爲尾。若是一個類 C 繼承自基類 B一、B二、……、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__.
相關文章
相關標籤/搜索