1. 程式人生 > >Python大神必須掌握的技能:多繼承、super與MRO演算法

Python大神必須掌握的技能:多繼承、super與MRO演算法

本文主要以Python3.x為例講解Python多繼承、super以及MRO演算法。

1. Python中的繼承

 

任何面向物件程式語言都會支援繼承,Python也不例外。但Python語言卻是少數幾個支援多繼承的面向物件程式語言(另一個著名的支援多繼承的程式語言是C++)。本文將深入闡述Python多繼承中經常用到的super,並且會展示一個你所不知道的super。

相信繼承的概念大家一定不會陌生。當類B從類A繼承後,B類就會繼承A類的所有非私有成員(由於Python沒有私有成員的概念,所以B類就會繼承A類的所有成員)。但有時需要在B類中直接訪問A類的成員,也就是子類需要呼叫父類的成員,在這種情況下,有如下兩種方法可以解決:

1. 在子類中直接通過父類名訪問父類中的成員

2. 在子類中通過super訪問父類中的成員

現在先說第一種方法。

 inhert.py

class A:
    def __init__(self):
        print('Start A')
        print('End A')
    def greet(self, name):
        return f'hello {name}'

class B(A):
    def __init__(self):
        print('Start B')
        # 直接通過父類名呼叫父類的構造方法
        A.__init__(self)
        # 呼叫父類的greet方法 
        print(A.greet(self,'Bill'))
        # 呼叫當前類的greet方法
        print(self.greet('Bill'))
        print('End B')
    # 覆蓋父類的greet方法
    def greet(self,name):
        return f'你好 {name}'
B()  # 建立B類的例項

 

這段程式碼的執行結果如圖1所示。

 

圖1

在B類中,通過A. __init__(self)和A.greet(self,'Bill')呼叫了父類(A類)的成員。在Python2.2之前,Python類只支援這種訪問父類成員的方式。儘管這種方式非常直接,但缺點是如果父類名改變,這就意味著所有使用父類名的子類都需要改變,如果某個類的子類非常多,就可能會增加非常多的程式碼維護工作量。所以從Python2.2開始,又增加了一種新的訪問父類的方式,這就是本文主要介紹的super。當然,舊的方式也同樣支援。

2.引入super

為了儘可能避免在子類中直接使用父類的名字,從Python2.2開始支援super。super並不是一個函式或方法,而是一個類。super類的構造方法需要兩個引數:type和instance。其中type就是型別,例如,A、B等,instance就是B類或其子類的例項。至於為什麼要傳遞這個例項。後面會詳細介紹,總之,該例項與本文的另外一個重點MRO演算法有關。

先看下面的程式碼:

super1.py

class A:
    def __init__(self):
        print('Start A')
        print('End A')
    def greet(self, name):
        return f'hello {name}'


class B(A):
    def __init__(self):
        print('Start B')
        # 通過super呼叫父類的構造方法
        super(B, self).__init__()
        # 通過super呼叫父類的成員方法
        print(super(B, self).greet('Bill'))
        print(self.greet('Bill'))
        print('End B')
    def greet(self,name):
        return f'你好 {name}'


B()

執行這段程式碼,會輸出與圖1完全相同的效果。在B類中並沒有直接使用A類的名字,而是使用了super。如果A類的名字變化了,只需要修改B類的父類即可,並不需要修改B類內部的程式碼,這樣將大大減少維護B類的工作量。


可能有的同學會問,super的第2個引數的值為什麼是self呢?我們都知道,在Python中,self表示類本身的例項,那麼為什麼不是B()或是A()呢?首先這個例項要求必須是B或B的子類的例項,所以A()以及其他不相關類的例項自然就排除在外,那麼B()為什麼不行呢?其實從語義上來說,B()沒問題,但問題是這樣將產生無限遞迴的後果。也就是在B類的構造方法又呼叫了B的構造方法(B()表示呼叫B類的構造方法),而且沒有終止條件。所以這麼做的後果就是棧溢位。


儘管不能在B類構造方法內部直接建立B類的例項,但卻可以在外部建立好B類的例項或B類子類的例項,然後通過B類構造方法將該例項傳入,看下面的程式碼。
super2.py

class A:
    def __init__(self):
        print('Start A')
        print('End A')
class B(A):
    def __init__(self,c):
        print('Start B')
        # 將外部建立的C類的例項傳入super類的構造方法
        super(B, c).__init__()
        print('End B')
class C(B):
    def __init__(self, c):
        super(C,self).__init__(c)
c = C(None)
b = B(None)
B(b)
B(c)

這段程式碼在建立B例項之前,先建立了一個C類的例項以及一個傳入None的B例項。而在B類的構造方法中多了一個引數,用於傳入這個外部例項,並將這個外部例項作為super類構造方法的第2個引數傳入。由於在建立C類和B類例項時傳入了None,所以super類構造方法的第2個引數值也是None。這樣回就會導致super(B,c)無法呼叫父類(A類)的構造方法,這就相當於一個空操作(什麼都不會做),至於為什麼會這樣,後面講MRO演算法時就會一清二楚。

3. 多繼承,找到親爹好難啊

其實如果Python不支援多繼承,一切都好說,一切都好理解。但問題是,Python支援多繼承,這就使得繼承的問題變得撲朔迷離,尤其是對初學者,更是一頭霧水。對於多繼承來說,一個重要的問題就是:在多個父類擁有同名成員的情況下,在子類中訪問該成員,到底是呼叫哪一個父類的成員呢?  毫無疑問,只有一個父類會為子類提供這個成員,也就是子類的親爹。至於其他擁有同名成員的父類,與該子類毫無關係,儘管名義上都擁有該成員。

    現在用一個最簡單的多繼承程式來說明問題:

super3.py

class X1:
    def __init__(self):
        print('Start X1')
        print('End X1')


class X2:
    def __init__(self,c):
        print('Start X2')
        print('End X2')
class A(X1,X2):
    def __init__(self):
        print('Start A')
        super(A,self).__init__()
        print('End A')
A()

在這段程式碼中,X1和X2都是A的父類,而在A類的構造方法中使用super(A,self).__init__()呼叫了父類的構造方法。任何Python類的構造方法可能都是同名的,都是__init__。如果A類只有一個父類,一切都好說。但如果A類有2個或2個以上的父類,那麼到底呼叫哪一個父類的構造方法呢?
讀者可以先執行這段程式碼,會看到輸出如下的內容:

Start A
Start X1
End X1
End A

 

很明顯,A呼叫了X1的構造方法。讀者可以再做一個實驗,將X1和X2的順序調換一下,變成A(X2,X1),這時會輸出如下的內容:
Start A
Start X2
End X2
End A

 

     很明顯,這時A呼叫了X2的構造方法。從觀察執行結果可以找出一點規律,就是使用super(A,self),會呼叫A類的父類列表中第1個父類的成員(本例是X1)。那麼結果真是這樣嗎?

    下面再看一個更復雜的多繼承案例:

    super4.py

class X1:
    def __init__(self):
        print('Start X1')
        print('End X1')
class X2:
    def __init__(self):
        print('Start X2')
        print('End X2')
class A(X1,X2):
    def __init__(self):
        print('Start A')
        super(A,self).__init__()
        print('End A')
class B:
    def __init__(self):
        print('Start B')
        print('End B')
class C:
    def __init__(self):
        print('Start C')
        print('End C')
class D:
    def __init__(self):
        print('Start D')
        print('End D')
class MyClass1(B,A,C):
    def __init__(self):
        print('Start MyClass1')
        super(MyClass1,self).__init__()
        print('End MyClass1')
class MyClass2(MyClass1,D):
    def __init__(self):
        print('Start MyClass2')
        super(MyClass2,self).__init__()
        print('End MyClass2')
MyClass2()

這段程式碼的繼承關係比較複雜,可以用圖2來表示。

圖2

 

執行這段程式碼,會輸出如下內容:
Start MyClass2
Start MyClass1
Start B
End B
End MyClass1
End MyClass2

 

從輸出結果也可以再次驗證前面的推論,也就是super會呼叫父類列表中第一個父類的成員。如MyClass1是MyClass2的第1個父類,所以MyClass2類會呼叫MyClass1類的構造方法,而B類是MyClass1類的第一個父類,所以MyClass1類會呼叫B類的構造方法。

但這裡有一個問題,如果在MyClass2類中想呼叫D類的構造方法,在MyClass1類中想呼叫A類的構造方法,該怎麼辦呢?當然,可以直接使用父類名進行呼叫,那麼使用super應該如何呼叫。

其實Python編譯器在解析類時,會將當前類的所有父類(包括直接和間接父類)按一定的規則進行排序,然後會根據super類構造方法的第一個引數的值決定使用哪一個父類。那麼這個順序是怎樣的呢?

現在先不用管這個順序,先將圖2的繼承關係圖倒過來,變成圖3的多叉樹。

 

圖3

 

現在按深度優先遍歷這顆二叉樹,得到的遍歷結果如下:

MyClass2 > MyClass1 > B > A > X1 > X2 > C > D

 

假設在MyClass2中要呼叫B類的構造方法,那麼可以使用下面的程式碼。

super(MyClass1, self).__init__()

 

假設在MyClass2中要呼叫X2類的構造方法,那麼可以使用下面的程式碼。

super(X1, self).__init__()

 

從這個規律可以看出,選擇父類的規則是super類構造方法的第1個引數值在前面深度優先遍歷序列中對應類的下一個類。例如,super(X1,self)就會去尋找X1的下一個類,也就是X2。如果super類構造方法的第1個引數值正好是深度優先遍歷序列的最後一個類,本例是D,那麼super將不會選擇MyClass2的任何父類,也就是super什麼都不會做(相當於一條空語句)。到現在為止,我們好像已經清楚了前面提到的一些疑問的答案。例如,super類構造方法的第1個引數值其實是對繼承樹深度優先遍歷列表搜尋的key,而第2個引數值其實是用來得到這個列表的。但真相真的是這樣嗎?

4. MRO演算法

好像通過多叉樹的深度優先遍歷就可以解決父類的順序問題,但很多時候,類的繼承關係並不是傳統的樹,例如下面這段程式碼的繼承關係就是一個菱形。

super5.py 

class Base:
    def __init__(self):
        print('Start Base')
        print('End Base')
class A(Base):
    def __init__(self):
        print('Start A')
        super(A,self).__init__()
        print('End A')


class B(Base):
    def __init__(self):
        print('Start B')
        super(B, self).__init__()
        print('End B')
class C(A,B):
    def __init__(self):
        print('Start C')
        super(C, self).__init__()
        print('End C')
C()

執行結果如下:

Start C

Start A

Start B

Start Base

End Base

End B

End A

End C

 

這段程式碼的繼承關係如圖4所示。

圖4

你就算把圖倒過來,樣子仍然不會變,如圖5所示。

圖5

 

這壓根不是一顆多叉樹,有點像一個圖。對於圖6所示的繼承關係,是無法用深度優先遍歷得到父類的順序的,所以為了彌補深度優先遍歷的缺陷,有人提出了MRO演算法,MRO是Method Resolution Order三個單詞的縮寫。

 

那麼什麼是MRO演算法呢?

 

MRO演算法:

MRO演算法是一個典型的遞迴操作,現在假設有如下兩個函式:

1. mro:用於得到指定類的父類MRO列表。接收一個type引數,表示指定的類,如mro(C)

2. merge:用於合併多個父類列表,合併的規則如下:

如果一個父類列表的第一個元素,在其他父類列表中也是第一個元素,或不在其他父類列表中出現,則從所有待合併父類列表中刪除這個元素(不存在的不需要刪除),合併到當前的mro父類列表中。  現在拿前面的菱形繼承關係為例說明如何得到MRO序列。這個序列的第一個元素就是C。有如下公式:

mro(C) = [C] + merge(mro(A), mro(B) ,[A,B])

 

其中merge函式有3個引數,分別是mro(A)、mro(B)和[A,B],也就是要將A和B的序列和[A,B]合併。根據前面的規則,有如下推導過程

mro(C) = [C] + merge(mro(A), mro(B) ,[A,B])

= [C] + merge([A,Base],[B,Base] ,[A,B])

= [C,A] + merge([Base],[B,Base],[B])

= [C,A,B] + merge([Base],[Base])

= [C,A,B,Base]

 

所以最終C類對應的mro序列為[C,A,B,Base],讀者可以執行前面的程式碼,會得到如下的結果:

Start C

Start A

Start B

Start Base

End Base

End B

End A

End C

 

這也就是為什麼會依次呼叫A、B和Base類構造方法的原因,因為MRO序列就是按這個順序排列的。如果想呼叫B類的構造方法,需要使用super(A,self).__init()__,系統會在MRO序列中搜索A,然後會呼叫A的下一個類(也就是B)的構造方法。對於更復雜的繼承關係,使用MRO演算法自己計算MRO序列非常麻煩,所以可以使用mro方法直接輸出MRO序列,程式碼如下:

print(C.mro())

執行這行程式碼,會輸出如下內容:

[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class '__main__.Base'>, <class 'object'>]

 

下載本文完整原始碼,請關注“極客起源”公眾號,並輸入235254獲得下載地址。更多精彩技術文章,請關注“極客起源”公眾號

 

&n