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

morra發表於2016-11-09

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

對於支援繼承的程式語言來說,其方法(屬性)可能定義在當前類,也可能來自於基類,所以在方法呼叫時就需要對當前類和基類進行搜尋以確定方法所在的位置。而搜尋的順序就是所謂的「方法解析順序」(Method Resolution Order,或MRO)。對於只支援單繼承的語言來說,MRO 一般比較簡單;而對於 Python 這種支援多繼承的語言來說,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 中,新式類是唯一支援的類。

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

>>> 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()。這顯然不是我們希望的結果。

對於新式類而言,所有的類都繼承自 object,所以「菱形繼承」是非常普遍的現象,因此不可能採用這種 MRO 方式。

Python 2.2 的新式類 MRO

為解決經典類 MRO 所存在的問題,Python 2.2 針對新式類提出了一種新的 MRO 計算方式:在定義類時就計算出該類的 MRO 並將其作為類的屬性。因此新式類可以直接通過__mro__屬性獲取類的 MRO。
Python 2.2 的新式類 MRO 計算方式和經典類 MRO 的計算方式非常相似:它仍然採用從左至右的深度優先遍歷,但是如果遍歷中出現重複的類,只保留最後一個。重新考慮上面「菱形繼承」的例子,由於新式類繼承自 object 因此類圖稍有改變[新式類菱形繼承]:

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

按照深度遍歷,其順序為 [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 方式已經能夠解決「菱形繼承」問題,再讓我們看個稍微複雜點的例子:

Python的方法解析順序(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 繼承自基類 B1、B2、……、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 的順序)。
我們再看一個沒有衝突的例子:

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

計算過程如下:

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__.

相關文章