天天看點

Python 方法解析順序(MRO)

前幾天我在 B 站錄制《Python 基礎教程》(第 3 版)示範視訊,我說到 Python 一個子類同時繼承多個父類的時候,如果多個父類有同名方法,子類應該調用哪一個父類的同名方法,這取決于子類查找多個父類的方法的順序,我們把這個順序稱之為方法解析順序(MRO),MRO 的實作算法非常的複雜,效果也很好,雖然書上說不需要為此擔心,但是還是需要講一下這個順序,不然可能會得不到你想要的結果。

為什麼不去 B 站講這個

有些人可能會問,為什麼我不去 B 站(我的 B 站昵稱文末會給出)講,偏要在微信公衆号說這個,因為我的 B 站針對的是 0 基礎的人,而這個 MRO 算法确實非常複雜,其本質就是圖這種資料結構的一個應用,我猜測我講了沒多少人聽得懂。而微信公衆号可不一樣,因為已經有兩大開發者社群在同步我的微信公衆号文章,這兩大開發者社群中大多數都是技術牛人,是以我選擇先讓看我文章的技術牛人先學會這個,是以現在微信公衆号講一下。逛 B 站的菜鳥們,你們盡管放心,這個問題已經收錄在我的遺留問題裡面了,我并沒有抛棄你們,後面我會在 B 站講這個方法解析順序(當然講之前會給你們補計算機基礎知識)。當然,即使書上沒有提及這個東西,我後面也還是會給你們補計算機基礎知識,因為從第 10 章第 3 節開始就已經開始涉及計算機基礎知識了,如果你沒有計算機基礎知識這一節你自己看書你有 70% 的内容可能看不懂,特别是第 4 小節和第 7 小節。

菱形繼承

我們先來看一下比較簡單的菱形繼承對應的 UML 類圖,如圖所示。

Python 方法解析順序(MRO)

接下來再看一下這個類圖對應的代碼,這裡我隻定義了三個類,object 被隐式繼承,是以不作聲明。

from pprint import pprint

class V1:    pass

class V2:    pass

class V3(V1, V2):    pass

pprint(V3.mro())           

複制

大家也都看到了,檢視方法解析順序直接用類調用 mro 方法并輸出即可,我們來看看輸出的結果,如圖所示。

Python 方法解析順序(MRO)

我們可以發現順序是 V3,V2,V1,object。如果把類圖抽象成資料結構的有向圖,這有點像廣度優先周遊,同時也有點像拓撲排序。具體是哪一個現在還是看不出來的,我們來把類圖變一下,如圖所示。

Python 方法解析順序(MRO)

其對應的代碼如下所示。

from pprint import pprint

class V1:    pass

class V2(V1):    pass

class V3(V1, V2):    pass

pprint(V3.mro())           

複制

運作一下看看,如圖所示。

Python 方法解析順序(MRO)

我們可以發現對方不想和你說話,并向你抛出了一個異常。那麼這是不是意味着它就沒有辦法實作之前的 UML 類圖呢?其實還是有辦法的,非常的簡單,我們把類 V3 繼承的兩個類順序換一下就行了,類圖還是之前的類圖,代碼也就 V3 需要修改,V1 和 V2 不需要修改,是以我隻給出 V3 的代碼,如下所示。

class V3(V2, V1):    pass           

複制

我們修改好之後繼續運作一下看看,如圖所示。

Python 方法解析順序(MRO)

我們還是看不出來是廣度優先周遊還是拓撲排序中的哪一個,但是我們可以得出一個非常重要的結論:繼承多個類的時候,我們需要把具體的類放在前面,抽象的類放在後面。在這裡 V2 繼承了 V1,是以 V2 比 V1 更具體,是以 V3 繼承這兩個類需要先寫 V2 後寫 V1。我們或許早就發現了,上面的繼承有一些多餘,V3 隻繼承 V2 和繼承 V2 和 V1 難道不是一回事嗎?确實是一回事,但是我必須用這種方法來找出這個順序究竟是廣度優先周遊還是拓撲排序,下面我們來看一個更複雜的例子,總共有 10 個類。

更複雜的例子

在這裡我們來看一個更複雜的例子,先上 UML 類圖。

Python 方法解析順序(MRO)

其對應的代碼如下所示。

from pprint import pprint

class V1:    pass

class V2:    pass

class V3(V1, V2):    pass

class V4(V3):    pass

class V5(V3):    pass

class V6(V3):    pass

class V7(V4, V5, V6):    pass

class V8(V7):    pass

class V9(V7):    pass

class V10(V8, V9, V1):    pass

pprint(V10.mro())           

複制

運作一下可以發現結果如下圖所示。

Python 方法解析順序(MRO)

我們可以發現,其順序絕對不可能是廣度優先周遊,因為如果是廣度優先周遊,通路完 V8 和 V9 之後就要通路 V1,而不是通路 V7。那麼拓撲排序對不對呢?其實可以帶進去看,拓撲排序是對的。但是這樣的圖對應的拓撲排序可不止一個,為什麼多次運作結果一樣?這個原因非常簡單,我們可以發現,V10 之後可以是 V8,V9 這樣的順序,也可以是 V9,V8 這樣的順序,為什麼每次都是 V8,V9 這個順序呢?大家去看一下 V10 這個類的定義就知道了,因為 V10 繼承三個類,即 V8,V9 和 V1。而且我上面寫的順序也是 V8,V9,V1。是以順序隻可能是 V8,V9(V1 這個多餘的我們不去管它)這樣的順序,不可能是 V9,V8 這樣的順序。

總結

通過上面對 Python 方法解析順序(MRO)的講解,我們可以得出以下兩個結論:

  1. 繼承多個類的時候要把越具體的類寫在越前面,越抽象的類寫在越後面。
  2. 方法解析順序就是拓撲排序外加一件事:先寫先排。