方法解析順序 MRO
面向對象中有一個重要特性是繼承,如果是單重繼承,要調用一個方法,只要按照繼承的順序查找基類即可。但多重繼承時,MRO算法的選擇(即基類的搜索順序)非常微妙。
Python先后有三種不同的MRO:經典方式、Python2.2 新式算法、Python2.3 新式算法(C3)。Python 3中只保留了最后一種,即C3算法。
經典方式: 非常簡單,深度優先,按定義從左到右
例如:菱形繼承結構,按經典方式,d類MRO為dbaca。缺點是如果c類重寫了a類中得方法,c類的方法將不會被調用到。此問題即本地優先順序問題
class a
:
pass
class b(a) : pass
class c(a) : pass
class d(b,c) : pass
class b(a) : pass
class c(a) : pass
class d(b,c) : pass
新式算法:還是經典方式,但出現重復的,只保留最后一個。上面的例子,MRO為 dbca。問題 單調性。
比如d繼承b,c 且b在c的前面。如果f繼承d,那么f的mro中也應該和d的一樣b在c的前面。單調性即繼承時要保持順序。
現在e繼承c,b 且c在b的前面。f繼承d,e時,bc的順序就沒法決定了。無論怎樣排都違反了單調性。
C3 算法:MRO是一個有序列表L,在類被創建時就計算出來。
L(Child(Base1,Base2)) = [ Child + merge( L(Base1) , L(Base2) , Base1Base2 )]
L(object) = [ object ]
L的性質:結果為列表,列表中至少有一個元素即類自己。
+ : 添加到列表的末尾,即 [ A + B ] = [ A,B ]
merge: ① 如果列表空則結束,非空 讀merge中第一個列表的表頭,
② 查看該表頭是否在 merge中所有列表的表尾中。
②-->③ 不在,則 放入 最終的L中,並從merge中的所有列表中刪除,然后 回到①中
②-->④ 在,查看 當前列表是否是merge中的最后一個列表
④-->⑤ 不是 ,跳過當前列表,讀merge中下一個列表的表頭,然后 回到 ②中
④-->⑥ 是,異常。類定義失敗。
表頭: 列表的第一個元素
表尾: 列表中表頭以外的元素集合(可以為空)
merge 簡單的說即尋找合法表頭(也就是不在表尾中的表頭),如果所有表中都未找到合法表頭則異常。
如(例2):
L(O)= O # O為 object 簡寫,省略[]符號,即 [object]
L(E(O)) = E + merge( L(O) , O )
= E + merge( O , O )
= E + O = EO
L ( F( O ) ) = FO
L ( D ( O ) ) = DO
L ( B ( D,E) ) = B + merge( L(D) , L(E), DE )
= B + merge( DO, EO, DE ) # 代入前面的結果,因為類的MRO是在類建立時計算出來的,所以 基類的MRO是已知的。
= B + D + merge( O, EO , E )
= B + D + E + merge( O , O )
= BDEO
L ( C ( D,F )) = CDFO
L ( A( B,C ) ) = A + merge( L(B), L(C), BC )
= A + merge( BDEO, CDFO, BC)
= A + B + merge( DEO, CDFO, C ) # 找到了合法表頭B,回到第一個列表繼續找, 第一個表頭D不合法, 找第二個,第二個C合法
= A + B + C + merge( DEO, CDFO , C ) # 找到了合法表頭C,回到第一個列表繼續找
= A + B + C + merge( DEO, DFO ) # 找到了合法表頭D,回到第一個列表繼續找
= A + B + C + D + merge( EO, FO )
= ABCDEFO
問題: 沒有異常情況下的C3算法 等效於 從左到右 的 廣度優先 算法嗎?
答案不是。
如(例3): O為所有類的基類,A為我們要計算MRO的類。A直接繼承自B,C 。B繼承D,D繼承O。
-- B --- D ---
A-- - C -------- O
整個繼承樹有四層,按繼承關系,BC 在同一層,都是A的直接父類。
C3 結果為 ABDCO, 而廣度為 ABCDO。
區別在於 ②-->③-->① 這里。
根據C3,一個類的MRO: 表頭顯然即是類自己,表尾為所有基類
第一次進行merge時,merge中的列表的表頭顯然都是A的直接父類,處於同一層次。
第一個列表的第二元素則是該列表表頭的基類,根據與A的繼承關系,顯然和第二個列表的表頭不在同一層次。
C3進行完第③步后,回到①繼續讀第一個列表的表頭,沒有讀 同層次的,即第二個列表的表頭。於是出現區別。
為什么 前面那個例子 這恰好 和 廣度 優先 算法結果相同?
答案是 merge中的 另一個路徑 ②-->④-->⑤。
這兩條路徑的區別即是C 的層次問題,也就是BC,CD的層次關系。
對A而言邏輯上BC處於同一層次,D是B的下一層,”看起來 D也應該是C的下一層。“
實際上不是,C3算法中C是B的下一個(A的直接父類,左右關系決定),同時D也是B的下一個(BD繼承關系決定)。
在這種關系下,既然DC都是B的下一個,也就是DC處於同一層次,根據左右規則 顯然 D前C后。
也就是 除非在另一個列表的輔助下明確D是C的下一層,如果 D不在C的表尾中,那么 D 不會比C的層次低。
如 (例4): A(B,C,F) B(D) F(D) MRO為 ABCFDO
A 添加了直接父類F,由於左右關系C在F的上一個,由於繼承關系F在D的上一個。所以C在D的前面。
MRO 實際 就是 離散數學中的全序問題,在 繼承關系中的 層次 由於 MRO中左右優先的規則而被改變(從A的父類角度)。
不過 ,如果 我們 從 O的子類來看 層次問題,如CD都是O的直接子類,而AB都不是直接子類。
這時,結果是對的,也就是 C3中的層次 是以 到O的最長步長為其所在的層次,同層次從左到右。
但 不管 從什么角度,在最終結果中 去掉左右順序后的繼承關系的順序是確定的。
也就是離散數學中的偏序,通過左右的先后順序,將偏序變成全序。
全序還可以換個說法是 存在一條路徑遍歷全部節點而不重復。
從動作上看就是捏住兩端,拉直。(每個節點間繩子具有最大彈性,最小為兩個節點多路徑下的最大步長)
【2】參考:
http://python3.blogspot.com/2010/07/method-resolution-order.html (該文參考python作者維護的python歷史博客)
