Python多繼承的坑與MRO C3廣度優先算法

雲棲號資訊:【點擊查看更多行業資訊
在這裏您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!


前言

繼承(英語:inheritance)是面向對象軟件技術當中的一個概念。如果一個類別B“繼承自”另一個類別A,就把這個B稱爲“A的子類”,而把A稱爲“B的父類別”也可以稱“A是B的超類”。繼承可以使得子類具有父類別的各種屬性和方法,而不需要再次編寫相同的代碼。在令子類別繼承父類別的同時,可以重新定義某些屬性,並重寫某些方法,即覆蓋父類別的原有屬性和方法,使其獲得與父類別不同的功能。另外,爲子類追加新的屬性和方法也是常見的做法。 一般靜態的面向對象編程語言,繼承屬於靜態的,意即在子類的行爲在編譯期就已經決定,無法在運行期擴展。

有些編程語言支持多重繼承,即一個子類可以同時有多個父類,比如C++編程語言;而在有些編程語言中,一個子類只能繼承自一個父類,比如Java編程語言,這時可以透過實現接口來實現與多重繼承相似的效果。

現今面向對象程序設計技巧中,繼承並非以繼承類別的“行爲”爲主,而是繼承類別的“類型”,使得組件的類型一致。另外在設計模式中提到一個守則,“多用合成,少用繼承”,此守則也是用來處理繼承無法在運行期動態擴展行爲的遺憾。

Python的繼承

Python有單繼承與多繼承。單繼承即子類繼承於一個類,多繼承即子類繼承於多個類,多繼承會比較少遇到,本章節主要講單繼承。

單繼承

在繼承中基類的構造方法(init()方法)不會被自動調用,它需要在其派生類的構造方法中親自專門調用。

在調用基類的方法時,需要加上基類的類名前綴,且需要帶上self參數變量。而在類中調用普通函數時並不需要帶上self參數

Python 總是首先查找對應類的方法,如果它不能在派生類中找到對應的方法,它纔開始到基類中逐個查找。(先在本類中查找調用的方法,找不到纔去基類中找) Python單繼承的案例:

class Animal(object): 
   def __init__(self, name, age):
       self.name = name
       self.age = age

   def call(self):
       print(self.name, '會叫')

class Cat(Animal):
   def __init__(self,name,age,sex):
       super(Cat, self).__init__(name,age)  # 不要忘記從Animal類執行初始化
       self.sex=sex

if __name__ == '__main__':  
   c = Cat('喵喵', 2, '男')  
   c.call()  
   
# 輸出 喵喵 會叫 ,Cat繼承了父類Animal的方法 

多繼承

對於多繼承,一個子類可以用多個父類的方法,這裏我們先給一個案例,再來詳解如果多個父類存在同名方法、父類中又有父類的同名方法時的執行順序 demo:

class Plant:
    def __init__(self, color):
        print("init Plant start")
        self.color = color
        print("init Plant end")

    def show(self):
        print("The Plant color is:", self.color)


class Fruit:
    def __init__(self, color):
        print("init Fruit start")
        self.color = color
        print("init Fruit end")

    def show(self):
        print("The Fruit color is:", self.color)


class Melon(Fruit):
    def __init__(self, color):
        print("init Melon start")
        super(Melon, self).__init__(color)
        self.color = color
        print("init Melon end")

    def show(self):
        print("The Melon color is:", self.color)


class Mango(Fruit, Plant):
    def __init__(self, color):
        print("init Mango start")
        Plant.__init__(self, color)
        Fruit.__init__(self, color)
        self.color = color
        print("init Mango end")

    def show_color(self):
        print("The Mango color is:", self.color)


class Watermelon(Melon, Plant):
    def __init__(self, color):
        print("init Watermelon start")
        Melon.__init__(self, color)
        Plant.__init__(self, color)
        self.color = color
        print("init Watermelon end")

    def show_color(self):
        print("The Watermelon color is:", self.color)


if __name__ == "__main__":
    mango = Mango("yellow")
    mango.show()

    watermelon = Watermelon("red")
    watermelon.show()

我們定義了植物Plant類和水果Fruit類,然後定義瓜Melon類繼承水果類,定義一個芒果Mango類繼承Fruit和Plant類,定義一個西瓜Watermelon類繼承Melon類和Plant類。 執行結果爲:

init Mango start
init Plant start
init Plant end
init Fruit start
init Fruit end
init Mango end
The Fruit color is: yellow

init Watermelon start
init Melon start
init Fruit start
init Fruit end
init Melon end
init Plant start
init Plant end
init Watermelon end
The Melon color is: red

多繼承的方法執行順序

從上面的案例我們可以看出:

Mango類即使初始化順序先初始化了Plant,再初始化Fruit類,但是執行的同名方法的順序仍然按定義該類是括號中的繼承順序;

Watermelon類也是按括號中的繼承順序來執行同名方法,但是執行的Melon類同名方法中,即使有該執行的Melon類的父類Fruit類也有同名方法,但還是優先執行該Melon類,這與一般的單繼承規則一致。 總結如下:

  • 定義派生類時,需要注意圓括號中繼承父類的順序,若是父類中有相同的方法名,而在子類使用時未指定,python從左至右搜索 即方法在子類中未找到時,從左到右查找父類中是否包含方法;
  • 如果繼承的父類中,該父類還繼承了別的父類,則調用同名方法時是調用最先訪問到的同名方法;
  • 支持多層父類繼承,子類會繼承父類所有的屬性和方法,包括父類的父類的所有屬性 和 方法。

多繼承時通過super方法初始化

單繼承中我們往往會使用super方法來初始化父類init方法,因爲super方法可以防止同一個類被實例化多次。 但是這在多繼承中往往會出現問題,我們把上面的例子改爲如下demo:

class Plant(object):
    def __init__(self, color):
        print("init Plant start")
        self.color = color
        print("init Plant end")

    def show(self):
        print("The Plant color is:", self.color)


class Fruit(object):
    def __init__(self, color):
        print("init Fruit start")
        self.color = color
        print("init Fruit end")

    def show(self):
        print("The Fruit color is:", self.color)


class Melon(Fruit):
    def __init__(self, color):
        print("init Melon start")
        super(Melon, self).__init__(color)
        self.color = color
        print("init Melon end")

    def show(self):
        print("The Melon color is:", self.color)


class Mango(Fruit, Plant):
    def __init__(self, color):
        print("init Mango start")
        super(Mango, self).__init__(color)
        self.color = color
        print("init Mango end")

    def show_color(self):
        print("The Mango color is:", self.color)


class Watermelon(Melon, Plant):
    def __init__(self, color):
        print("init Watermelon start")
        super(Watermelon, self).__init__(color)
        self.color = color
        print("init Watermelon end")

    def show_color(self):
        print("The Watermelon color is:", self.color)


if __name__ == "__main__":
    mango = Mango("yellow")
    mango.show()

    watermelon = Watermelon("red")
    watermelon.show()

可以看到,我們只是把上面Mango類和Watermelon類初始化父類的方法改爲了super方式,但是執行結果卻如下:

init Mango start
init Fruit start
init Fruit end
init Mango end
The Fruit color is: yellow
init Watermelon start
init Melon start
init Fruit start
init Fruit end
init Melon end
init Watermelon end
The Melon color is: red

可以看到,兩個實例中繼承順序排在後面的Plant類都沒有被實例化,所以多繼承中採用super是會有問題的,它只會實例化一次,所以super類並不適合使用在多繼承場合。 其實導致這個問題的原因是Python中的一個MRO(Method Resolution Order)繼承調用順序的算法,我們要修復上面的問題,可以在Fruit類中加入super方法,把Fruit類改爲:

class Fruit(object):
    def __init__(self, color):
        super().__init__(color)
        print("init Fruit start")
        self.color = color
        print("init Fruit end")

    def show(self):
        print("The Fruit color is:", self.color)

就可以達到實例化兩個類的作用了

關於Python的mro方法

Python 中針對 類 提供了一個內置屬性 mro 可以查看方法搜索順序 MRO 是 method resolution order,主要用於在多繼承時判斷 方法、屬性 的調用 路徑 Python2.3以上,MRO算法就已經改爲了廣度優先,防止最頂級的類被多次實例化而耗費資源:

從C.mro的值可以看出, Python的方法解析優先級從高到低爲:

  • 實例本身(instance)
  • 類(class)
  • super class, 繼承關係越近, 越先定義, 優先級越高. 例如我們執行print(Mango.__mro__)會得到如下返回: (, , , )

Python多繼承的注意事項

在單繼承場景:super().init等同於類名.init;

在多繼承場景,super方法只能保證第一個父類被初始化,除非每一個繼承的父類都實現super初始化,最終初始化了根類Object類;

另外,子類從多個父類派生,而子類又沒有自己的構造函數時:

1. 按順序繼承,哪個父類在最前面且它又有自己的構造函數,就繼承它的構造函數;
2. 如果最前面第一個父類沒有構造函數,則繼承第2個的構造函數,第2個沒有的話,再往後找,以此類推。

【雲棲號在線課堂】每天都有產品技術專家分享!
課程地址:https://yqh.aliyun.com/live

立即加入社羣,與專家面對面,及時瞭解課程最新動態!
【雲棲號在線課堂 社羣】https://c.tb.cn/F3.Z8gvnK

原文發佈時間:2020-08-03
本文作者:henry_czh
本文來自:“掘金”,瞭解相關信息可以關注“掘金”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章