C3 線(xiàn)性化算法與 MRO之Python中的多繼承
Python 中的方法解析順序(Method Resolution Order, MRO)定義了多繼承存在時(shí) Python 解釋器查找函數(shù)解析的正確方式。當(dāng) Python 版本從 2.2 發(fā)展到 2.3 再到現(xiàn)在的 Python 3,MRO算法也隨之發(fā)生了相應(yīng)的變化。這種變化在很多時(shí)候影響了我們使用不同版本 Python 編程的過(guò)程。
什么是 MRO
MRO 全稱(chēng)方法解析順序(Method Resolution Order)。它定義了 Python 中多繼承存在的情況下,解釋器查找函數(shù)解析的具體順序。什么是函數(shù)解析順序?我們首先用一個(gè)簡(jiǎn)單的例子來(lái)說(shuō)明。請(qǐng)仔細(xì)看下面代碼:
class A():
def who_am_i(self):
print("I am A")
class B(A):
pass
class C(A):
def who_am_i(self):
print("I am C")
class D(B,C):
pass
d = D()
如果我問(wèn)在 Python 2 中使用 D 的實(shí)例調(diào)用 d.who_am_i(),究竟執(zhí)行的是 A 中的 who_am_i() 還是 C 中的 who_am_i(),我想百分之九十以上的人都會(huì)不假思索地回答:肯定是 C 中的 who_am_i(),因?yàn)?C 是 D 的直接父類(lèi)。然而,如果你把代碼用 Python 2 運(yùn)行一下就可以看到 d.who_am_i() 打印的是 I am A。
是不是覺(jué)得很混亂很奇怪?感到奇怪就對(duì)了!?。?/p>
這個(gè)例子充分展示了 MRO 的作用:決定基類(lèi)中的函數(shù)到底應(yīng)該以什么樣的順序調(diào)用父類(lèi)中的函數(shù)??梢悦鞔_地說(shuō),Python 發(fā)展到現(xiàn)在,MRO 算法已經(jīng)不是一個(gè)憑借著執(zhí)行結(jié)果就能猜出來(lái)的算法了。如果沒(méi)有深入到 MRO 算法的細(xì)節(jié),稍微復(fù)雜一點(diǎn)的繼承關(guān)系和方法調(diào)用都能徹底繞暈?zāi)恪?/p>
New-style Class vs. Old-style Class
在介紹不同版本的 MRO 算法之前,我們有必要簡(jiǎn)單地回顧一下 Python 中類(lèi)定義方式的發(fā)展歷史。盡管在 Python 3 中已經(jīng)廢除了老式的類(lèi)定義方式和 MRO 算法,但對(duì)于仍然廣泛使用的 Python 2 來(lái)說(shuō),不同的類(lèi)定義方式與 MRO 算法之間具有緊密的聯(lián)系。了解這一點(diǎn)將幫助我們從 Python 2 向 Python 3 遷移時(shí)不會(huì)出現(xiàn)莫名其妙的錯(cuò)誤。
在 Python 2.1 及以前,我們定義一個(gè)類(lèi)的時(shí)候往往是這個(gè)樣子(我們把這種類(lèi)稱(chēng)為 old-style class):
class A:
def __init__(self):
pass
Python 2.2 引入了新的模型對(duì)象(new-style class),其建議新的類(lèi)型通過(guò)如下方式定義:
class A(object):
def __init__(self):
pass
注意后一種定義方式顯示注明類(lèi) A 繼承自 object。Python 2.3 及后續(xù)版本為了保持向下兼容,同時(shí)提供以上兩種類(lèi)定義用以區(qū)分 old-style class 和 new-style class。Python 3 則完全廢棄了 old-style class 的概念,不論你通過(guò)以上哪種方式書(shū)寫(xiě)代碼,Python 3 都將明確認(rèn)為類(lèi) A 繼承自 object。這里我們只是引入 old-style 和 new-style 的概念,如果你對(duì)他們的區(qū)別感興趣,可以自行看 stackoverflow 上有關(guān)該問(wèn)題的解釋。
理解 old-style class 的 MRO
我們使用前文中的類(lèi)繼承關(guān)系來(lái)介紹 Python 2 中針對(duì) old-style class 的 MRO 算法。如果你在前面執(zhí)行過(guò)那段代碼,你可以看到調(diào)用 d.who_am_i() 打印的應(yīng)該是 I am A。為什么 Python 2 的解釋器在確定 D 中的函數(shù)調(diào)用時(shí)要先搜索 A 而不是先搜索 D 的直接父類(lèi) C 呢?
這是由于 Python 2 對(duì)于 old-style class 使用了非常簡(jiǎn)單的基于深度優(yōu)先遍歷的 MRO 算法(關(guān)于深度優(yōu)先遍歷,我想大家肯定都不陌生)。當(dāng)一個(gè)類(lèi)繼承自多個(gè)類(lèi)時(shí),Python 2 按照從左到右的順序深度遍歷類(lèi)的繼承圖,從而確定類(lèi)中函數(shù)的調(diào)用順序。這個(gè)過(guò)程具體如下:
- 檢查當(dāng)前的類(lèi)里面是否有該函數(shù),如果有則直接調(diào)用。
- 檢查當(dāng)前類(lèi)的第一個(gè)父類(lèi)里面是否有該函數(shù),如果沒(méi)有則檢查父類(lèi)的第一個(gè)父類(lèi)是否有該函數(shù),以此遞歸深度遍歷。
- 如果沒(méi)有則回溯一層,檢查下一個(gè)父類(lèi)里面是否有該函數(shù)并按照 2 中的方式遞歸。
上面的過(guò)程與標(biāo)準(zhǔn)的深度優(yōu)先遍歷只有一點(diǎn)細(xì)微的差別:步驟 2 總是按照繼承列表中類(lèi)的先后順序來(lái)選擇分支的遍歷順序。具體來(lái)說(shuō),類(lèi) D 的繼承列表中類(lèi)順序?yàn)?B, C,因此,類(lèi) D 按照先遍歷 B 分支再遍歷 C 分支的順序來(lái)確定 MRO。
我們繼續(xù)用第一個(gè)例子中的函數(shù)繼承圖來(lái)說(shuō)明這個(gè)過(guò)程:

按照上述深度遞歸的方式,函數(shù) d.who_am_i() 調(diào)用的搜索順序是 D, B, A, C, A。由于一個(gè)類(lèi)不能兩次出現(xiàn),因此在搜索路徑中去除掉重復(fù)出現(xiàn)的 A,得到最終的方法解析順序是 D, B, A, C。這樣一來(lái)你就明白了為什么 d.who_am_i() 打印的是 I am A 了。
在 Python 2 中,我們可以通過(guò)如下方式來(lái)查看 old-style class 的 MRO:
>>> import inspect >>> inspect.getmro(D)
理解 new-style class 的 MRO
從上面的結(jié)果可以看到,使用深度優(yōu)先遍歷的查找算法并不合理。因此,Python 3 以及 Python 2 針對(duì) new-style class 采用了新的 MRO 算法。如果你使用 Python 3 重新運(yùn)行一遍上述腳本,你就可以看到函數(shù) d.who_am_i() 的打印結(jié)果是 I am C。
>>> d.who_am_i() I am C >>> D.__mro__ (<class 'test.D'>, <class 'test.B'>, <class 'test.C'>, <class 'test.A'>, <class 'object'>)
新算法與基于深度遍歷的算法類(lèi)似,但是不同在于新算法會(huì)對(duì)深度優(yōu)先遍歷得到的搜索路徑進(jìn)行額外的檢查。其從左到右掃描得到的搜索路徑,對(duì)于每一個(gè)節(jié)點(diǎn)解釋器都會(huì)判斷該節(jié)點(diǎn)是不是好的節(jié)點(diǎn)。如果不是好的節(jié)點(diǎn),那么將其從當(dāng)前的搜索路徑中移除。
那么問(wèn)題在于,什么是一個(gè)好的節(jié)點(diǎn)?我們說(shuō) N 是一個(gè)好的節(jié)點(diǎn)當(dāng)且僅當(dāng)搜索路徑中 N 之后的節(jié)點(diǎn)都不繼承自 N。我們還以上述的類(lèi)繼承圖為例,按照深度優(yōu)先遍歷得到類(lèi) D 中函數(shù)的搜索路徑 D, B, A, C, A。之后 Python 解釋器從左向右檢查時(shí)發(fā)現(xiàn)第三個(gè)節(jié)點(diǎn) A 不是一個(gè)好的節(jié)點(diǎn),因?yàn)?A 之后的節(jié)點(diǎn) C 繼承自 A。因此其將 A 從搜索路徑中移除,然后得到最后的調(diào)用順序 D, B, C, A。
采用上述算法,D 中的函數(shù)調(diào)用將優(yōu)先查找其直接父類(lèi) B 和 C 中的相應(yīng)函數(shù)。
C3線(xiàn)性化算法
上一小結(jié)我們從直觀(guān)上概述了針對(duì) new-style class 的 MRO 算法過(guò)程。事實(shí)上這個(gè)算法有一個(gè)明確的名字 C3 linearization。下面我們給出其形式化的計(jì)算過(guò)程。

上面的過(guò)程看起來(lái)好像很復(fù)雜,我們用一個(gè)例子來(lái)具體執(zhí)行一下,你就會(huì)覺(jué)得其實(shí)還是挺簡(jiǎn)單的。假設(shè)我們有如下的一個(gè)類(lèi)繼承關(guān)系:
class X():
def who_am_i(self):
print("I am a X")
class Y():
def who_am_i(self):
print("I am a Y")
class A(X, Y):
def who_am_i(self):
print("I am a A")
class B(Y, X):
def who_am_i(self):
print("I am a B")
class F(A, B):
def who_am_i(self):
print("I am a F")

Traceback (most recent call last):
File "test.py", line 17, in <module>
class F(A, B):
TypeError: Cannot create a consistent method resolution
order (MRO) for bases X, Y
參考文獻(xiàn) Python Tutorial: Understanding Python MRO - Class search path The Python 2.3 Method Resolution Order C3 linearization
到此這篇關(guān)于C3 線(xiàn)性化算法與 MRO之Python中的多繼承的文章就介紹到這了,更多相關(guān)Python多繼承內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Pycharm打印大數(shù)據(jù)文件顯示不全的解決方法
這篇文章主要介紹了Pycharm打印大數(shù)據(jù)文件顯示不全的解決方法,昨晚寫(xiě)了個(gè)小爬蟲(chóng),簡(jiǎn)單分析下發(fā)現(xiàn)可以修改請(qǐng)求的url,直接獲取所有目標(biāo)的數(shù)據(jù),想先打印在控制臺(tái)看看,發(fā)現(xiàn)打印的數(shù)據(jù)不全,所以本文記錄了一下解決方法,需要的朋友可以參考下2024-03-03
Python中第三方庫(kù)Requests庫(kù)的高級(jí)用法詳解
雖然Python的標(biāo)準(zhǔn)庫(kù)中urllib2模塊已經(jīng)包含了平常我們使用的大多數(shù)功能,但是它的API使用起來(lái)讓人實(shí)在感覺(jué)不好。它已經(jīng)不適合現(xiàn)在的時(shí)代,不適合現(xiàn)代的互聯(lián)網(wǎng)了。而Requests的誕生讓我們有了更好的選擇。本文就介紹了Python中第三方庫(kù)Requests庫(kù)的高級(jí)用法。2017-03-03
如何利用Python給自己的頭像加一個(gè)小國(guó)旗(小月餅)
這篇文章主要給大家介紹了關(guān)于如何利用Python給自己的頭像加一個(gè)小國(guó)旗(小月餅)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
python+mysql實(shí)現(xiàn)個(gè)人論文管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了python+mysql實(shí)現(xiàn)個(gè)人論文管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10
PyQt5入門(mén)之基于QListWidget版本實(shí)現(xiàn)圖片縮略圖列表功能
這篇文章主要介紹了PyQt5入門(mén)之基于QListWidget版本實(shí)現(xiàn)圖片縮略圖列表功能,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-09-09
python中join與os.path.join()函數(shù)實(shí)例詳解
os.path.join()函數(shù)用于路徑拼接文件路徑,下面這篇文章主要給大家介紹了關(guān)于python中join與os.path.join()函數(shù)的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-03-03
使用Python編寫(xiě)一個(gè)桌面便簽應(yīng)用
ChatGPT的編程能力也不差,本文將一步一步提出要求,讓ChatGPT根據(jù)我們的要求,編寫(xiě)出一個(gè)可用的,可打包運(yùn)行的桌面便簽,感興趣的可以了解一下2023-06-06
pycharm實(shí)現(xiàn)設(shè)置自動(dòng)的參數(shù)注釋標(biāo)識(shí)
這篇文章主要介紹了pycharm實(shí)現(xiàn)設(shè)置自動(dòng)的參數(shù)注釋標(biāo)識(shí),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02

