1. 程式人生 > 實用技巧 >20.繼承應用(在子類派生重用父類功能(super)),繼承實現原理(繼承順序、菱形問題、繼承原理、Mixins機制)、組合

20.繼承應用(在子類派生重用父類功能(super)),繼承實現原理(繼承順序、菱形問題、繼承原理、Mixins機制)、組合

  • 引子
  • 繼承應用

  • 在子類派生的新方法中重用父類的功能(super)

  • 繼承實現原理

  • 繼承順序

  • 菱形問題

  • 繼承原理

  • Mixins機制

  • 組合


  • 繼承應用

    類與類之間的繼承指的是什麼’是’什麼的關係(比如人類,豬類,猴類都是動物類)。子類可以繼承/遺傳父類所有的屬性,因而繼承可以用來解決類與類之間的程式碼重用性問題。比如我們按照定義Student類的方式再定義一個Teacher類

class Student:           # 定義學生類
    school = "虹橋校區"   # 冗餘共同屬性

    def __init__(self,name,age,gender):  # 學生與老師有冗餘部分
        self.name = name
        self.age = age
        self.gender = gender

    def choose(self):
        print("%s 選課成功" %self.name)

stu1 = Student("jack",18,"male")
stu2 = Student("tom",19,"male")
stu3 = Student('lili',29,"female")

class Teacher:          # 定義老師類
    school = "虹橋校區"

    def __init__(self,name,age,gender,level):  # 老師與學生功能多一個level
        self.name = name
        self.age = age
        self.gender = gender
        self.level = level

    def score(self):
        print("%s 正在為學生打分" %self.name)

tea1 = Teacher('egon',18,"male",10)
tea2 = Teacher('lxx',38,"male",3)

從上面看出:類Teacher與Student之間存在重複的程式碼,老師與學生都是人類,所以我們可以得出如下繼承關係,實現程式碼重用
  • 在子類派生的新方法中重用父類的功能:

  • 方式一: 指名道姓地引用某一個類的函式,與繼承無關

class People:
    school = "虹橋校區"    # 將冗餘資料放到父類當中

    def __init__(self,name,age,gender):  # 將學生類和老師類的共同屬性放到父類中間解決冗餘問題
        self.name = name
        self.age = age
        self.gender = gender

class Student(People):
    def choose(self):
        print("%s 選課成功" %self.name)

class Teacher(People):
         #      定義一個__init__函式是形參  
		 #      空物件,'egon',18,"male",10
    def __init__(self,name,age,gender,level):  # 老師類多一個level就需要保留
       # 呼叫父類的__init__是函式(實參)不是方法,為老師類的__init__進行傳參
        People.__init__(self,name,age,gender)  # 在子類的派生當中重用父類功能

        self.level = level

    def score(self):
        print("%s 正在為學生打分" %self.name)


stu1 = Student("jack",18,"male")
stu2 = Student("tom",19,"male")
stu3 = Student('lili',29,"female")


tea1 = Teacher('egon',18,"male",10)  # 空物件,'egon',18,"male",10
tea2 = Teacher('lxx',38,"male",3)


# print(stu1.school)
# print(stu1.name)
# print(stu1.age)
# print(stu1.gender)
print(tea1.__dict__)
  • 方式二: super()返回一個特殊的物件,該物件會參考發起屬性查詢的那一個類的mro列表,去當前類的父類中找屬性,嚴格依賴繼承

class People:
    school = "虹橋校區"

    def __init__(self,name,age,gender):
        self.name = name
        self.age = age
        self.gender = gender

class Teacher(People):
    #            空物件,'egon',18,"male",10
    def __init__(self,name,age,gender,level):
        # People.__init__(self,name,age,gender)
        super(Teacher,self).__init__(name,age,gender)

        self.level = level

    def score(self):
        print("%s 正在為學生打分" %self.name)


tea1 = Teacher('egon',18,"male",10)  # 空物件,'egon',18,"male",10
print(tea1.__dict__)


# 案例:
class A:  # [A,object]
    def test(self):
        print("from A")
        super().test()
class B:
    def test(self):
        print('from B')
class C(A,B):  # [C,A,B,object]
    pass

# obj=C()
# obj.test()

obj1 = A()
obj1.test()
  • 繼承的實現原理

    繼承順序

    Python中子類可以同時繼承多個父類,如A(B,C,D)

    如果繼承關係為非菱形結構,則會按照先找B這一條分支,然後再找C這一條分支,最後找D這一條分支的順序直到找到我們想要的屬性

    如果繼承關係為菱形結構,那麼屬性的查詢方式有兩種,分別是:深度優先和廣度優先

# 查詢順序
class A(object):
    def test(self):
        print('from A')

class B(A):
    def test(self):
        print('from B')

class C(A):
    def test(self):
        print('from C')

class D(B):
    def test(self):
        print('from D')

class E(C):
    def test(self):
        print('from E')

class F(D,E):
    # def test(self):
    #     print('from F')
    pass
f1=F()
f1.test()
print(F.__mro__) #只有新式才有這個屬性可以檢視線性列表,經典類沒有這個屬性

# 新式類繼承順序:F->D->B->E->C->A
# 經典類繼承順序:F->D->B->A->E->C
# python3中統一都是新式類
# pyhon2中才分新式類與經典類

  • 菱形問題

    大多數面嚮物件語言都不支援多繼承,而在Python中,一個子類是可以同時繼承多個父類的,這固然可以帶來一個子類可以對多個不同父類加以重用的好處,但也有可能引發著名的 Diamond problem菱形問題(或稱鑽石問題,有時候也被稱為“死亡鑽石”),菱形其實就是對下面這種繼承結構的形象比喻
    菱形繼承/死亡鑽石:一個子類繼承的多條分支最終匯聚一個非object的類上
class G: # 在python2中,未繼承object的類及其子類,都是經典類
    def test(self):
        print('from G')

class E(G):
    def test(self):
        print('from E')

class F(G):
    def test(self):
        print('from F')

class B(E):
    # def test(self):
    #     print('from B')
    pass

class C(F):
    def test(self):
        print('from C')

class D(G):
    def test(self):
        print('from D')

class A(B,C,D):
    # def test(self):
    #     print('from A')
    pass

obj = A()  # A->B->E->C->F->D->G->object
# print(A.mro())

obj.test()
  • 繼承原理

  • python到底是如何實現繼承的呢? 對於你定義的每一個類,Python都會計算出一個方法解析順序(MRO)列表,該MRO列表就是一個簡單的所有基類的線性順序列表,如下

A.mro()  # 等同於A.__mro__
[<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.F'>, <class '__main__.D'>, <class '__main__.G'>, <class 'object'>]
  • 為了實現繼承,python會在MRO列表上從左到右開始查詢基類,直到找到第一個匹配這個屬性的類為止。 而這個MRO列表的構造是通過一個C3線性化演算法來實現的。我們不去深究這個演算法的數學原理,它實際上就是合併所有父類的MRO列表並遵循如下三條準則:

    • 1.子類會先於父類被檢查
      2.多個父類會根據它們在列表中的順序被檢查
      3.如果對下一個類存在兩個合法的選擇,選擇第一個父類
  • Mixins機制

    繼承表達的是一個is-a的關係(什麼是什麼的關係)

    單繼承可以很好的表達人類的邏輯(一個子類繼承一個父類符合邏輯且清晰)
    多繼承就有點亂,在人類的世界觀裡,一個物品不可能是多種不同的東西,因此多重繼承
    在人類的世界觀裡是說不通的,它僅僅只是程式碼層面的邏輯(還可能導致菱形問題)
    民航飛機、直升飛機、轎車都是一個(is-a)交通工具,前兩者都有一個功能是飛行fly,但是轎車沒有,所以如下所示
# 我們把飛行功能放到交通工具這個父類中是不合理的
class Vehicle:  # 交通工具
    def fly(self):
        '''
        飛行功能相應的程式碼        
        '''
        print("I am flying")

class CivilAircraft(Vehicle):  # 民航飛機
    pass

class Helicopter(Vehicle):  # 直升飛機
    pass

class Car(Vehicle):  # 汽車並不會飛,但按照上述繼承關係,汽車也能飛了
    pass
Python語言沒有介面功能,但Python提供了Mixins機制,簡單來說Mixins機制指的是子類混合(mixin)不同類的功能,而這些類採用統一的命名規範(例如Mixin字尾),以此標識這些類只是用來混合功能的,並不是用來標識子類的從屬"is-a"關係的,所以Mixins機制本質仍是多繼承,但同樣遵守”is-a”關係,如下
class Vehicle:   # 交通工具
    pass

class FlyableMixin:     # 定義一個飛行功能
    def fly(self):
        print('flying')

class CivilAircraft(FlyableMixin,Vehicle):  # 民航飛機  繼承了Flyablemixin就使用它的功能
    pass

class Helicopter(FlyableMixin,Vehicle):    # 直升飛機
    pass

class Car(Vehicle):             # 小汽車   不繼承就不使用它的功能,也不影響
    pass

# ps: 採用這種編碼規範(如命名規範)來解決具體的問題是python慣用的套路,大家都照這種規範來
# 這種規範叫什麼呢?
# 建議你不要用多繼承,如果你要用這個繼承,用來表達你歸屬關係的那個類(當然也是最複雜的
# 那個類)往多繼承的最右邊去放(Vehicle)。用來新增功能的類往左邊放而且改名Mixin為後
# 綴的名字(FlyableMixin)這種類的特點是:這種類裡面就放功能,功能的特點是能獨立執行
總結:可以看到,上面的CivilAircraft、Helicopter類實現了多繼承,不過它繼承的第一個類我們起名
FlyableMixin,而不是Flyable,這個並不影響功能,但是會告訴後來讀程式碼的人,這個類是一個Mixin類
單詞表示混入(mix-in),這個名字給人一種提示性的效果,只要以Mixin這種命名方式的類就知道這個類只
是為我這個類來新增功能的,只要繼承這個類就說明混合了這個功能
  • 組合

    組合:一個物件的屬性值是指向另外一個類的物件,稱為類的組合

    組合與繼承都是用來解決程式碼的重用問題的,不同的是:

    繼承是一種什麼“是”什麼的關係
    組合是則一種“有”什麼的關係
    比如學校有學生,老師,學生有多門課程,課程有:類別、週期、學費。學生選python課程或linux課程,或多門課程,應該使用組合,如下示例
class People:
    school = "虹橋校區"

    def __init__(self,name,age,gender):
        self.name = name
        self.age = age
        self.gender = gender

class Student(People):
    def choose(self):
        print("%s 選課成功" %self.name)
        
class Teacher(People):
    #            空物件,'egon',18,"male",10
    def __init__(self,name,age,gender,level):
        People.__init__(self,name,age,gender)

        self.level = level

    def score(self):
        print("%s 正在為學生打分" %self.name)

class Course:
    def __init__(self,name,price,period):
        self.name = name
        self.price = price
        self.period =period

    def tell(self):
        print('課程資訊<%s:%s:%s>' %(self.name,self.price,self.period))
        
# 課程屬性
python = Course("python全棧開發",19800,"6mons")
linux = Course("linux",19000,"5mons")

# 學生屬性
stu1 = Student("jack",18,"male")
stu2 = Student("tom",19,"male")
stu3 = Student('lili',29,"female")

# 老師屬性
tea1 = Teacher('egon',18,"male",10) 
tea2 = Teacher('lxx',38,"male",3)


stu1.course = python   # 假如規定只有pyhton一門課程就直接等於python,這就叫組合
stu1.course.tell()     # 直接檢視課程資訊
stu1.courses = []             # 給學生1新增多門課程
stu1.courses.append(python)   # 給學生1新增python課程
stu1.courses.append(linux)    # 給學生1新增linux課程

# 此時物件stu1集物件獨有的屬性、Student類中的內容、Course類中的內容於一身(都可以訪問到),
# 是一個高度整合的產物

print(stu1.courses)   # 檢視學生1所學的多門課程的每一門課程的資訊
for course_obj in stu1.courses:
    course_obj.tell()