20.繼承應用(在子類派生重用父類功能(super)),繼承實現原理(繼承順序、菱形問題、繼承原理、Mixins機制)、組合
阿新 • • 發佈:2021-01-12
-
引子
-
繼承應用
-
在子類派生的新方法中重用父類的功能(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()