Python 2.3 方法解析順序¶
注意
這是一份歷史文件,作為官方文件的附錄提供。這裡討論的方法解析順序是在 Python 2.3 中引入的,但它仍然在更高版本中使用——包括 Python 3。
- 摘要:
本文件面向希望理解 Python 2.3 中使用的 C3 方法解析順序的 Python 程式設計師。雖然它不是為新手準備的,但它具有很強的教學性,包含許多示例。我沒有發現其他公開提供的具有相同範圍的文件,因此它應該是有用的。
免責宣告
我將本文件捐贈給 Python 軟體基金會,根據 Python 2.3 許可證。像往常一樣,在這種情況下,我警告讀者,以下內容應該是正確的,但我沒有給出任何保證。請您自擔風險使用!
致謝
所有在 Python 郵件列表中向我傳送支援的人。Paul Foley 指出了各種不準確之處,並促使我添加了關於區域性優先順序排序的部分。David Goodger 幫助進行了 reStructuredText 格式化。David Mertz 幫助進行了編輯。最後,Guido van Rossum 非常熱情地將本文件新增到官方 Python 2.3 主頁。
開始¶
Felix qui potuit rerum cognoscere causas – Virgilius
一切都始於 Samuele Pedroni 在 Python 開發郵件列表中釋出的一篇文章 [1]。在他的帖子中,Samuele 表明 Python 2.2 方法解析順序不是單調的,他建議用 C3 方法解析順序代替它。Guido 同意他的觀點,因此現在 Python 2.3 使用 C3。C3 方法本身與 Python 無關,因為它是由研究 Dylan 的人發明的,並且在為 lispers 準備的論文中進行了描述 [2]。本文為希望瞭解更改原因的 Pythonistas 提供了 C3 演算法(希望)可讀的討論。
首先,請允許我指出,我接下來要說的僅適用於 Python 2.2 中引入的新式類:經典類保持其舊的方法解析順序,深度優先然後從左到右。因此,對於經典類,不會破壞舊程式碼;即使原則上可能會破壞 Python 2.2 新式類的程式碼,但在實踐中,C3 解析順序與 Python 2.2 方法解析順序不同的情況非常罕見,因此預計不會真正破壞程式碼。所以
不要害怕!
此外,除非你大量使用多重繼承並且具有非平凡的層次結構,否則你不需要理解 C3 演算法,你可以輕鬆跳過本文。另一方面,如果你真的想了解多重繼承的工作原理,那麼本文適合你。好訊息是,事情並不像你想象的那麼複雜。
讓我從一些基本定義開始。
給定一個複雜的多重繼承層次結構中的類 C,指定重寫方法的順序,即指定 C 的祖先的順序,是一項非平凡的任務。
類 C 的祖先列表,包括類本身,從最近的祖先到最遠的祖先排序,稱為類優先順序列表或 C 的線性化。
方法解析順序 (MRO) 是一組構建線性化的規則。在 Python 文獻中,習慣用法“C 的 MRO”也用作類 C 的線性化的同義詞。
例如,在單繼承層次結構的情況下,如果 C 是 C1 的子類,而 C1 是 C2 的子類,則 C 的線性化就是列表 [C, C1, C2]。然而,對於多重繼承層次結構,線性化的構建更加繁瑣,因為它更難以構建尊重區域性優先順序排序和單調性的線性化。
我將在稍後討論區域性優先順序排序,但我可以在這裡給出單調性的定義。當以下情況為真時,MRO 是單調的:如果 C1 在 C 的線性化中先於 C2,則 C1 在 C 的任何子類的線性化中先於 C2。否則,派生新類的無害操作可能會更改方法的解析順序,從而可能引入非常細微的錯誤。稍後將顯示發生這種情況的示例。
並非所有類都允許線性化。在複雜的層次結構中,在某些情況下,無法派生一個類,使其線性化尊重所有期望的屬性。
在這裡,我給出一個這種情況的例子。考慮層次結構
>>> O = object
>>> class X(O): pass
>>> class Y(O): pass
>>> class A(X,Y): pass
>>> class B(Y,X): pass
可以用以下繼承圖表示,其中我用 O 表示 object
類,它是新式類任何層次結構的開始
----------- | | | O | | / \ | - X Y / | / | / | / |/ A B \ / ?
在這種情況下,無法從 A 和 B 派生新類 C,因為 X 在 A 中先於 Y,而 Y 在 B 中先於 X,因此方法解析順序在 C 中將是模糊的。
Python 2.3 在這種情況下引發異常(TypeError: MRO conflict among bases Y, X),禁止幼稚的程式設計師建立模糊的層次結構。而 Python 2.2 不會引發異常,而是選擇一個特別的順序(在這種情況下為 CABXYO)。
C3 方法解析順序¶
讓我介紹一些簡單的符號,這些符號將在以下討論中很有用。我將使用速記符號
C1 C2 ... CN
來表示類列表 [C1, C2, …, CN]。
列表的頭是它的第一個元素
head = C1
而尾是列表的其餘部分
tail = C2 ... CN.
我還將使用符號
C + (C1 C2 ... CN) = C C1 C2 ... CN
來表示列表 [C] + [C1, C2, …,CN] 的總和。
現在我可以解釋 MRO 在 Python 2.3 中是如何工作的。
考慮多重繼承層次結構中的類 C,其中 C 從基類 B1、B2、…、BN 繼承。我們要計算類 C 的線性化 L[C]。規則如下
C 的線性化是 C 加上父類的線性化和父類列表的合併的總和。
用符號表示
L[C(B1 ... BN)] = C + merge(L[B1] ... L[BN], B1 ... BN)
特別是,如果 C 是沒有父類的 object
類,則線性化很簡單
L[object] = object.
然而,一般來說,必須按照以下規定計算合併
取第一個列表的頭,即 L[B1][0];如果這個頭不在任何其他列表的尾部,則將其新增到 C 的線性化中並將其從合併列表中的列表中刪除,否則檢視下一個列表的頭並取出它,如果它是一個好的頭。然後重複操作,直到刪除所有類或無法找到好的頭。在這種情況下,無法構建合併,Python 2.3 將拒絕建立類 C 並引發異常。
如果可以保留順序,此規定可確保合併操作保留順序。另一方面,如果無法保留順序(如上面討論的嚴重順序不一致的示例中那樣),則無法計算合併。
如果 C 只有一個父類(單繼承),則合併的計算很簡單;在這種情況下
L[C(B)] = C + merge(L[B],B) = C + L[B]
然而,在多重繼承的情況下,事情更加繁瑣,我不指望你沒有幾個例子就可以理解規則 ;-)
示例¶
第一個例子。考慮以下層次結構
>>> 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
在這種情況下,繼承圖可以繪製為
6 --- Level 3 | O | (more general) / --- \ / | \ | / | \ | / | \ | --- --- --- | Level 2 3 | D | 4| E | | F | 5 | --- --- --- | \ \ _ / | | \ / \ _ | | \ / \ | | --- --- | Level 1 1 | B | | C | 2 | --- --- | \ / | \ / \ / --- Level 0 0 | A | (more specialized) ---
O、D、E 和 F 的線性化很簡單
L[O] = O
L[D] = D O
L[E] = E O
L[F] = F O
B 的線性化可以計算為
L[B] = B + merge(DO, EO, DE)
我們看到 D 是一個好的頭,因此我們取它,然後我們被簡化為計算 merge(O,EO,E)
。現在 O 不是一個好的頭,因為它在序列 EO 的尾部。在這種情況下,規則說我們必須跳到下一個序列。然後我們看到 E 是一個好的頭;我們取它,然後我們被簡化為計算 merge(O,O)
,這給出 O。因此
L[B] = B D E O
使用相同的步驟,可以找到
L[C] = C + merge(DO,FO,DF)
= C + D + merge(O,FO,F)
= C + D + F + merge(O,O)
= C D F O
現在我們可以計算
L[A] = A + merge(BDEO,CDFO,BC)
= A + B + merge(DEO,CDFO,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,O)
= A B C D E F O
在此示例中,線性化根據繼承級別以相當好的方式排序,這意味著較低的級別(即更專業的類)具有更高的優先順序(請參閱繼承圖)。然而,這不是一般情況。
我留給讀者一個練習,計算我的第二個示例的線性化
>>> 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
與前一個示例的唯一區別是將 B(D,E) 更改為 B(E,D);然而,即使是如此小的修改也會完全改變層次結構的順序
6 --- Level 3 | O | / --- \ / | \ / | \ / | \ --- --- --- Level 2 2 | E | 4 | D | | F | 5 --- --- --- \ / \ / \ / \ / \ / \ / --- --- Level 1 1 | B | | C | 3 --- --- \ / \ / --- Level 0 0 | A | ---
請注意,層次結構第二層的類 E 先於層次結構第一層的類 C,即 E 比 C 更專業,即使它處於更高的級別。
一個懶惰的程式設計師可以直接從 Python 2.2 獲取 MRO,因為在這種情況下它與 Python 2.3 線性化一致。只需呼叫類 A 的 mro()
方法即可
>>> A.mro()
[<class 'A'>, <class 'B'>, <class 'E'>,
<class 'C'>, <class 'D'>, <class 'F'>,
<class 'object'>]
最後,讓我考慮第一部分中討論的示例,該示例涉及嚴重的順序不一致。在這種情況下,直接計算 O、X、Y、A 和 B 的線性化
L[O] = 0 L[X] = X O L[Y] = Y O L[A] = A X Y O L[B] = B Y X O
然而,無法為從 A 和 B 繼承的類 C 計算線性化
L[C] = C + merge(AXYO, BYXO, AB)
= C + A + merge(XYO, BYXO, B)
= C + A + B + merge(XYO, YXO)
此時,我們無法合併列表 XYO 和 YXO,因為 X 在 YXO 的尾部,而 Y 在 XYO 的尾部:因此沒有好的頭,C3 演算法停止。Python 2.3 引發錯誤並拒絕建立類 C。
錯誤的 Method Resolution Order(方法解析順序)¶
當 MRO 破壞了區域性優先順序排序和單調性等基本屬性時,它就是錯誤的。在本節中,我將展示經典類和 Python 2.2 中新式類的 MRO 都是錯誤的。
從區域性優先順序排序開始更容易理解。考慮以下示例
>>> F=type('Food',(),{'remember2buy':'spam'})
>>> E=type('Eggs',(F,),{'remember2buy':'eggs'})
>>> G=type('GoodFood',(F,E),{}) # under Python 2.3 this is an error!
以及繼承圖
O | (buy spam) F | \ | E (buy eggs) | / G (buy eggs or spam ?)
我們看到類 G 繼承自 F 和 E,其中 F 在 E 之前:因此我們期望屬性 G.remember2buy 被 F.rembermer2buy 繼承,而不是被 E.remember2buy 繼承:然而 Python 2.2 給出了
>>> G.remember2buy
'eggs'
這是對區域性優先順序排序的破壞,因為區域性優先順序列表(即 G 的父類列表)中的順序沒有在 G 的 Python 2.2 線性化中保留。
L[G,P22]= G E F object # F *follows* E
有人可能會說,F 在 Python 2.2 線性化中位於 E 之後的原因是 F 比 E 更不特殊,因為 F 是 E 的超類;然而,破壞區域性優先順序排序是非常反直覺且容易出錯的。尤其如此,因為它與舊式類不同
>>> class F: remember2buy='spam'
>>> class E(F): remember2buy='eggs'
>>> class G(F,E): pass
>>> G.remember2buy
'spam'
在這種情況下,MRO 是 GFEF,並且區域性優先順序順序被保留。
作為一般規則,應該避免像前面這樣的層次結構,因為不清楚 F 是否應該覆蓋 E,或者反之亦然。Python 2.3 透過在建立類 G 時引發異常來解決歧義,有效地阻止程式設計師生成有歧義的層次結構。原因是 C3 演算法在合併時失敗
merge(FO,EFO,FE)
無法計算,因為 F 在 EFO 的尾部,而 E 在 FE 的尾部。
真正的解決方案是設計一個非歧義的層次結構,即從 E 和 F(更具體地在前面)而不是從 F 和 E 派生 G;在這種情況下,MRO 是 GEF,毫無疑問。
O | F (spam) / | (eggs) E | \ | G (eggs, no doubt)
Python 2.3 強制程式設計師編寫良好的層次結構(或者至少是不太容易出錯的層次結構)。
順便提一下,我想指出的是,Python 2.3 演算法足夠聰明,可以識別明顯的錯誤,例如父類列表中的類重複
>>> class A(object): pass
>>> class C(A,A): pass # error
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: duplicate base class A
在這種情況下,Python 2.2(對於經典類和新式類)不會引發任何異常。
最後,我想指出我們從這個例子中吸取的兩個教訓
儘管名稱如此,MRO 決定了屬性的解析順序,而不僅僅是方法;
Python 愛好者的預設食物是 spam! (但你已經知道了 ;-)
在討論了局部優先順序排序的問題之後,現在讓我考慮單調性的問題。我的目標是證明經典類的 MRO 和 Python 2.2 新式類的 MRO 都不是單調的。
要證明經典類的 MRO 是非單調的相當簡單,只需檢視菱形圖
C / \ / \ A B \ / \ / D
很容易發現不一致之處
L[B,P21] = B C # B precedes C : B's methods win
L[D,P21] = D A C B C # B follows C : C's methods win!
另一方面,Python 2.2 和 2.3 MRO 沒有問題,它們都給出
L[D] = D A B C
Guido 在他的文章 [3] 中指出,經典 MRO 在實踐中並沒有那麼糟糕,因為通常可以避免經典類的菱形結構。但是所有新式類都繼承自 object
,因此菱形結構是不可避免的,並且不一致性會出現在每個多重繼承圖中。
Python 2.2 的 MRO 使得破壞單調性變得困難,但並非不可能。以下示例(最初由 Samuele Pedroni 提供)表明 Python 2.2 的 MRO 是非單調的
>>> class A(object): pass
>>> class B(object): pass
>>> class C(object): pass
>>> class D(object): pass
>>> class E(object): pass
>>> class K1(A,B,C): pass
>>> class K2(D,B,E): pass
>>> class K3(D,A): pass
>>> class Z(K1,K2,K3): pass
以下是根據 C3 MRO 的線性化(讀者應該驗證這些線性化並繪製繼承圖作為練習 ;-)
L[A] = A O
L[B] = B O
L[C] = C O
L[D] = D O
L[E] = E O
L[K1]= K1 A B C O
L[K2]= K2 D B E O
L[K3]= K3 D A O
L[Z] = Z K1 K2 K3 D A B C E O
Python 2.2 為 A、B、C、D、E、K1、K2 和 K3 給出了完全相同的線性化,但為 Z 給出了不同的線性化
L[Z,P22] = Z K1 K3 A K2 D B C E O
很明顯,這個線性化是錯誤的,因為 A 出現在 D 之前,而 K3 的線性化中 A 出現在 D 之後。換句話說,在 K3 中,D 派生的方法覆蓋了 A 派生的方法,但在 Z(仍然是 K3 的子類)中,A 派生的方法覆蓋了 D 派生的方法!這違反了單調性。此外,Z 的 Python 2.2 線性化也與區域性優先順序排序不一致,因為類 Z 的區域性優先順序列表是 [K1, K2, K3](K2 在 K3 之前),而在 Z 的線性化中,K2 在 K3 之後。這些問題解釋了為什麼 2.2 規則被否決,而支援 C3 規則。
結束¶
本節是為那些跳過所有前面的章節並直接跳到結尾的讀者準備的。本節也是為那些不想動腦筋的懶惰程式設計師準備的。最後,它是為那些有點自負的程式設計師準備的,否則他們就不會閱讀有關多重繼承層次結構中 C3 方法解析順序的論文 ;-) 這三種美德加在一起(而不是分開)值得獎勵:獎勵是一個簡短的 Python 2.2 指令碼,它允許您計算 2.3 MRO,而不會對您的頭腦造成任何風險。只需更改最後一行即可使用我在本文中討論的各種示例。
#<mro.py>
"""C3 algorithm by Samuele Pedroni (with readability enhanced by me)."""
class __metaclass__(type):
"All classes are metamagically modified to be nicely printed"
__repr__ = lambda cls: cls.__name__
class ex_2:
"Serious order disagreement" #From Guido
class O: pass
class X(O): pass
class Y(O): pass
class A(X,Y): pass
class B(Y,X): pass
try:
class Z(A,B): pass #creates Z(A,B) in Python 2.2
except TypeError:
pass # Z(A,B) cannot be created in Python 2.3
class ex_5:
"My first example"
class O: pass
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
class ex_6:
"My second example"
class O: pass
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
class ex_9:
"Difference between Python 2.2 MRO and C3" #From Samuele
class O: pass
class A(O): pass
class B(O): pass
class C(O): pass
class D(O): pass
class E(O): pass
class K1(A,B,C): pass
class K2(D,B,E): pass
class K3(D,A): pass
class Z(K1,K2,K3): pass
def merge(seqs):
print '\n\nCPL[%s]=%s' % (seqs[0][0],seqs),
res = []; i=0
while 1:
nonemptyseqs=[seq for seq in seqs if seq]
if not nonemptyseqs: return res
i+=1; print '\n',i,'round: candidates...',
for seq in nonemptyseqs: # find merge candidates among seq heads
cand = seq[0]; print ' ',cand,
nothead=[s for s in nonemptyseqs if cand in s[1:]]
if nothead: cand=None #reject candidate
else: break
if not cand: raise "Inconsistent hierarchy"
res.append(cand)
for seq in nonemptyseqs: # remove cand
if seq[0] == cand: del seq[0]
def mro(C):
"Compute the class precedence list (mro) according to C3"
return merge([[C]]+map(mro,C.__bases__)+[list(C.__bases__)])
def print_mro(C):
print '\nMRO[%s]=%s' % (C,mro(C))
print '\nP22 MRO[%s]=%s' % (C,C.mro())
print_mro(ex_9.Z)
#</mro.py>
就這些了,夥計們,
盡情享受!