Python - 物件導向程式設計 - MRO 方法搜尋順序

小菠蘿測試筆記 發表於 2021-09-06
Python 物件導向

為什麼會講 MRO?

  • 在講多繼承的時候:https://www.cnblogs.com/poloyy/p/15224912.html
  • 有講到, 當繼承的多個父類擁有同名屬性、方法,子類物件呼叫該屬性、方法時會呼叫哪個父類的屬性、方法呢?
  • 這就取決於 Python 的 MRO 了

 

什麼是 MRO

  • MRO,method resolution order,方法搜尋順序
  • 對於單繼承來說,MRO 很簡單,從當前類開始,逐個搜尋它的父類有沒有對應的屬性、方法
  • 所以 MRO 更多用在多繼承時判斷方法、屬性的呼叫路徑
  • Python 中針對類提供了一個內建屬性 __mro__ 可以檢視方法搜尋順序

 

實際程式碼

class A:
    def test(self):
        print("AAA-test")


class B:
    def test(self):
        print("BBB-test")

# 繼承了三個類,B、A、還有預設繼承的 object class C(B, A): ... # 通過類物件呼叫,不是例項物件! print(C.__mro__) # 輸出結果 (<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
  1. 在搜尋方法時,是按照 __mro__ 的輸出結果從左往右的順序查詢的
  2. 如果在當前類(Class C)中找到方法,就直接執行,不再搜尋
  3. 如果沒有找到,就查詢下一個類中(Class B)是否有對應的方法,如果找到,就直接執行,不再搜素
  4. 如果找到最後一個類(Class object)都沒有找到方法,程式報錯

 

類圖

Python - 物件導向程式設計 - MRO 方法搜尋順序

 

注意

其實 MRO 是涉及一個底層演算法的,下面來詳細講解一下

 

MRO 演算法

Python 發展到現在經歷了三種演算法

  1. 舊式類 MRO 演算法:從左往右,採用深度優先搜尋(DFS),從左往右的演算法,稱為舊式類的 MRO
  2. 新式類 MRO 演算法:自 Python 2.2 版本開始,新式類在採用深度優先搜尋演算法的基礎上,對其做了優化
  3. C3 演算法:自 Python 2.3 版本,對新式類採用了 C3 演算法;由於 Python 3.x 僅支援新式類,所以該版本只使用 C3 演算法

 

什麼是舊式類,新式類

https://www.cnblogs.com/poloyy/p/15226425.html

 

想深入瞭解 C3 演算法的可以看看官網

https://www.python.org/download/releases/2.3/mro/

 

舊式類 MRO 演算法

需要在 python2 環境下執行這段程式碼

 

實際程式碼

# 舊式類演算法
class A:
    def test(self):
        print("CommonA")


class B(A):
    pass


class C(A):
    def test(self):
        print("CommonC")


class D(B, C):
    pass


D().test()



# python2 下的執行結果
CommonA

Python - 物件導向程式設計 - MRO 方法搜尋順序

 

類圖

Python - 物件導向程式設計 - MRO 方法搜尋順序

 

分析

  • 通過類圖可以看到,此程式中的 4 個類是一個“菱形”繼承的關係
  • 當使用 D 類例項物件訪問 test() 方法時,根據深度優先演算法,搜尋順序為  D->B->A->C->A 
  • 因此,舊式類 MRO 演算法最先搜尋得到 test() 方法是在 A 類裡面,所以最終輸出結果為 CommonA

 

新式類 MRO 演算法

  • 為解決舊式類 MRO 演算法存在的問題,Python 2.2 版本推出了新的計算新式類 MRO 的方法
  • 它仍然採用從左至右的深度優先遍歷,但是如果遍歷中出現重複的類,只保留最後一個

 

以上面的程式碼栗子來講

  • 深度優先遍歷,搜尋順序為 D->B->A->C->A
  • 因為順序中有 2 個 A,因此只保留最後一個
  • 最終搜尋順序為 D->B->C->A 

 

新式 MRO 演算法的問題

雖然解決了舊式 MRO 演算法的問題,但可能會違反單調性原則

 

什麼是單調性原則?

在子類存在多繼承時,子類不能改變父類的 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 

 

分析不同類的 MRO

  • A: A->X->Y->object 
  • B: A->Y->X->object 
  • C: C->A->B->X->Y->object 

很明顯,B、C 中間的 X、Y 順序是相反的,就是說 B 被繼承時,它的搜尋順序會被改變,違反了單調性

 

在 python2 中執行這段程式碼的報錯

Python - 物件導向程式設計 - MRO 方法搜尋順序

 

在 python3 中執行這段程式碼的報錯 

Python - 物件導向程式設計 - MRO 方法搜尋順序

 

C3 MRO 演算法

  • 為解決前面兩個演算法的問題,Python 2.3 採用了 C3 方法來確定方法搜尋順序
  • 多數情況下,如果別人提到 Python 中的 MRO,指的都是 C3 演算法

 

將上面第一個栗子的程式碼放到 python3 中執行

class A:
    def test(self):
        print("CommonA")


class B(A):
    pass


class C(A):
    def test(self):
        print("CommonC")


class D(B, C):
    pass


D().test()


# 輸出結果
CommonC

 

簡單瞭解下 C3 演算法

以上面程式碼為栗子,C3 會把各個類的 MRO 等價為以下等式

  • A:L[A] = merge(A , object)
  • B:L[B] = B + merge(L[A] , A)
  • C:L[C] = C + merge(L[A] , A)
  • D:L[D] = D + merge(L[B] , L[C] , B , C)

 

瞭解一下:頭、尾

以 A 類為慄,merge() 包含的 A 成為 L[A] 的頭,剩餘元素(這裡只有 object)稱為尾

 

merge 的運算方式

  1. 將 merge 第一個列表的頭元素(如 L[A] 的頭),記作 H
  2. 如果 H 出現在 merge 其他列表的頭部,則將其輸出,並將其從所有列表中刪除
  3. 如果 H 只出現一次,那麼也將其輸出,並將其從所有列表中刪除
  4. 如果 H 出現在 merge 其他列表的非頭部,則取下一個列表的頭元素記作 H,然後回到步驟二
  5. 最後回到步驟一,重複以上步驟

重複以上步驟直到列表為空,則演算法結束;如果不能再找出可以輸出的元素,則丟擲異常

 

簡單類 MRO 的計算栗子

class B(object): pass

print(B.__mro__)


(<class '__main__.B'>, <class 'object'>)

  

MRO 計算方式

L[B] = L[B(object)]
     = B + merge(L[object])
     = B + L[object]
     = B object

  

單繼承 MRO 的計算栗子

# 計算 MRO
class B(object): pass

class C(B): pass

print(C.__mro__)


(<class '__main__.C'>, <class '__main__.B'>, <class 'object'>)

 

MRO 計算方式

L[C] = C + merge(L[B])
     = C + L[B]
     = C B object

 

多繼承 MRO 的計算栗子

O = object

class F(O): pass

class E(O): pass

class D(O): pass

class C(D, F): pass

class B(D, E): pass

class A(B, C): pass


print(C.__mro__)
print(B.__mro__)
print(A.__mro__)


# 輸出結果
(<class '__main__.C'>, <class '__main__.D'>, <class '__main__.F'>, <class 'object'>)
(<class '__main__.B'>, <class '__main__.D'>, <class '__main__.E'>, <class 'object'>)
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class '__main__.E'>, <class '__main__.F'>, <class 'object'>)

 

O 類、object 類 MRO 計算

L[O] = O = object

 

D、E、F 類 MRO 計算

L[D] = D + merge(L[O])
        = D O

 

C 類 MRO 計算

L[C] = L[C(D, F)]
     = C + merge(L[D], L[F], DF)
     # 從前面可知 L[D] 和 L[F] 的結果
     = C +  merge(DO, FO, DF)
     # 因為 D 是順序第一個並且在幾個包含 D 的 list 中是 head,
     # 所以這一次取 D 同時從列表中刪除 D
     = C + D + merge(O, FO, F)
     # 因為 O 雖然是順序第一個但在其他 list (FO)中是在尾部, 跳過
     # 改為檢查第二個list FO
     # F 是第二個 list 和其他 list 的 head
     # 取 F 同時從列表中刪除 F
     = C + D + F + merge(O)
     = C D F O

 

B 類 MRO 計算

L[B] = L[B(D, E)]
     = B + merge(L[D], L[E], DE)
     = B + merge(DO, EO, DE)
     = B + D + merge(O, EO, E)
     = B + D + E + merge(O)
     = B D E O

 

A 類 MRO 計算

L[A] = L[A(B,C)]
        = A + merge(L[B], L[C], BC)
        = A + merge( BDEO, CDFO, BC )
        = A + B + merge( DEO, CDFO, C )
        # D 在其他列表 CDFO 不是 head,所以跳過到下一個列表的 頭元素 C
        = A + B + C + merge( DEO, DFO )
        = A + B + C + D + merge( EO, FO )
        = A + B + C + D + E + merge( O, FO )
        = A + B + C + D + E + F + merge( O )
        = A B C D E F O

 

多繼承 MRO 的計算栗子二

O = object

class F(O): pass

class E(O): pass

class D(O): pass

class C(D, F): pass

class B(E, D): pass

class A(B, C): pass


print(C.__mro__)
print(B.__mro__)
print(A.__mro__)


# 輸出結果
(<class '__main__.C'>, <class '__main__.D'>, <class '__main__.F'>, <class 'object'>)
(<class '__main__.B'>, <class '__main__.E'>, <class '__main__.D'>, <class 'object'>)
(<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.D'>, <class '__main__.F'>, <class 'object'>)

 

O 類、object 類 MRO 計算

L[O] = O = object

 

D、E、F 類 MRO 計算

L[D] = D + merge(L[O])
        = D O

 

C 類 MRO 計算

L[C] = L[C(D, F)]
        = C + merge(L[D], L[F], DF)
        = C + merge(DO, FO, DF)
        = C + D + merge(O, FO, F)
        = C + D + F + merge(O)
        = C D F O

 

B 類 MRO 計算

L[B] = L[B(E, D)]
       = B + merge(L[E], L[D], ED)
       = B + merge(EO, DO, ED)
       = B + E + merge(O, DO, D)
       = B + E + D + merge(O)
       = B E D O

 

A 類 MRO 計算

L[A]  = L[A(B, C)]
        = A + merge(L[B], L[C], BC)
        = A + merge(BEDO, CDFO, BC)
        = A + B + merge(EDO, CDFO, C)
        = A + B + E + merge(DO,CDFO, C)
        = A + B + E + C + merge(O,DFO)
        = A + B + E + C + D + merge(O, FO)
        = A + B + E + C + D + F + merge(O)
        = A B E C D F O