【目錄】
一、繼承介紹
二、繼承與抽象
三、屬性查找
四、繼承的實作原理
1、菱形問題
2、繼承原理
3、深度優先和廣度優先
4、python Mixins機制
五、派生與方法重用
六、組合
一、繼承介紹
1、什么是繼承
(1)繼承是一種創建新類的方式,在python中,新建的類可以繼承一個或多個父類,
新建的類可稱為子類或派生類,父類又可稱為基類或超類
class ParentClass1: #定義父類 pass class ParentClass2: #定義父類 pass class SubClass1(ParentClass1): #單繼承 pass class SubClass2(ParentClass1,ParentClass2): #多繼承 pass
(2)查看類繼承的所有父類:通過類的內置屬性__bases__可以查看類繼承的所有父類
>>> SubClass2.__bases__ (<class '__main__.ParentClass1'>, <class '__main__.ParentClass2'>)
(3)經典類與新式類
-
在Python2中有經典類與新式類之分——
沒有顯式地繼承object類的類,以及該類的子類,都是經典類;
顯式地繼承object的類,以及該類的子類,都是新式類,
>>> ParentClass1.__bases__ (<class ‘object'>,) >>> ParentClass2.__bases__ (<class 'object'>,)
-
而在Python3中,即使沒有顯式地繼承object類,也會默認繼承該類,
因而在python3中統一都是新式類,關于經典類與新式類的區別,未完待續,
提示:object類提供了一些常用內置方法的實作,如用來在列印物件時回傳字串的內置方法__str__
2、為什么要繼承
為了解決類與類之間,代碼冗余的問題
3、python中多繼承的優缺點
# 優點:
子類可以同時遺傳多個父類的屬性,最大限度地重用代碼
# 缺點:
# 1、違背人的思維習慣:繼承表達的是一種什么"是"什么的關系
# 2、代碼可讀性會變差
# 3、不建議使用多繼承,有可能會引發可惡的菱形問題,擴展性變差,
# 如果真的涉及到一個子類不可避免地要重用多個父類的屬性,應該使用Mixins機制
二、繼承與抽象
1、繼承和抽象的理論方法
要找出類與類之間的繼承關系,需要先抽象,再繼承,
抽象即總結相似之處,總結物件之間的相似之處得到類(子類),
總結類與類之間的相似之處就可以得到父類,
2、如何實作繼承
【栗子】
以學生和老師的資訊管理為例,學生類與老師類之間存在資訊引數冗余問題:
# 示范1:類與類之間存在冗余問題 class Student: school='OLDBOY' def __init__(self,name,age,sex): self.name=name self.age=age self.sex=sex def choose_course(self): print('學生%s 正在選課' %self.name) class Teacher: school='OLDBOY' def __init__(self,name,age,sex,salary,level): self.name=name self.age=age self.sex=sex self.salary=salary self.level=level def score(self): print('老師 %s 正在給學生打分' %self.name)
基于繼承解決類與類之間的冗余問題:
# 示范2:基于繼承解決類與類之間的冗余問題 class OldboyPeople: school = 'OLDBOY' def __init__(self, name, age, sex): self.name = name self.age = age self.sex = sex class Student(OldboyPeople): def choose_course(self): print('學生%s 正在選課' % self.name) stu_obj = Student('lili', 18, 'female') print(stu_obj.__dict__) print(stu_obj.school) stu_obj.choose_course() class Teacher(OldboyPeople): # 老師的空物件,'egon',18,'male',3000,10 def __init__(self, name, age, sex, salary, level): # 指名道姓地跟父類OldboyPeople去要__init__ OldboyPeople.__init__(self,name,age, sex) self.salary = salary self.level = level def score(self): print('老師 %s 正在給學生打分' % self.name) tea_obj=Teacher('egon',18,'male',3000,10) print(tea_obj.__dict__) print(tea_obj.school) tea_obj.score() # 輸出結果: {'name': 'lili', 'age': 18, 'sex': 'female'} OLDBOY 學生lili 正在選課 {'name': 'egon', 'age': 18, 'sex': 'male', 'salary': 3000, 'level': 10} OLDBOY 老師 egon 正在給學生打分
三、屬性查找
有了繼承關系,物件在查找屬性時,先從物件自己的__dict__中找,如果沒有則去子類中找,然后再去父類中找……
1、單繼承背景下的屬性查找
【栗子1】
obj.f2()會在父類foo中找到f2,先列印 Foo.f2,
然后執行到self.f1(),即 obj.f1(),仍會按照:物件本身->類bar->父類foo的順序依次找下去,
在類bar中找到f1,因而列印結果為 Bar.f1
# 示范一: class Foo: def f1(self): print('Foo.f1') def f2(self): print('Foo.f2') self.f1() # obj.f1() # 呼叫類Bar中的f1—— 先從自身物件找,沒有再去自己的類Bar中有沒有f1,沒有再去繼承的類Foo中找
class Bar(Foo): def f1(self): print('Bar.f1') obj=Bar() obj.f2() # 輸出結果: # Foo.f2 # Bar.f1
【栗子2】如何實作子類和父類中,呼叫的 def f1(self) 不一樣,即在自己類中,就只呼叫自己類中的 def f1(self)
方法一:Foo.f1(self) # 呼叫當前類Foo中的f1
# # 示范二: class Foo: def f1(self): print('Foo.f1') def f2(self): print('Foo.f2') Foo.f1(self) # 呼叫當前類Foo中的f1 class Bar(Foo): def f1(self): print('Bar.f1') obj=Bar() obj.f2() #輸出結果: # Foo.f2 # Foo.f1
方法二:父類如果不想讓子類覆寫自己的方法,可以采用雙下劃線開頭的方式將方法設定為私有的
即 將父類中的 f1 隱藏起來,變為父類私有的
# 示范三: class Foo: def __f1(self): # _Foo__f1 print('Foo.f1') def f2(self): print('Foo.f2') self.__f1() # self._Foo__f1,# 呼叫當前類中的f1 class Bar(Foo): def __f1(self): # _Bar__f1 print('Bar.f1') obj=Bar() obj.f2() # 輸出結果: # Foo.f2 # Foo.f1
2、多繼承的屬性查找
基本原則不變,但會涉及到菱形問題,詳見后文分析,
四、繼承的實作原理
1、菱形問題
大多數面向物件語言都不支持多繼承,而在python中,一個子類是可以同時繼承多個父類的,
這固然可以帶來一個子類可以對多個不同父類加以重用的好處,
但也有可能引發著名的 diamond problem菱形問題(或稱鉆石問題,有時候也被稱為“死亡鉆石”),
菱形,其實就是對下面這種繼承結構的形象比喻:

a類在頂部,b類和c類分別位于其下方,d類在底部將兩者連接在一起形成菱形
這種繼承結構下導致的問題稱之為菱形問題:
如果a中有一個方法,b和/或c都重寫了該方法,而d沒有重寫它,那么d繼承的是哪個版本的方法:b的還是c的?如下所示:
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,C): pass obj = D() obj.test() # 結果為:from B
要想搞明白obj.test()是如何找到方法test的,需要了解python的繼承實作原理——
(提前透露查找順序——obj.test()的查找順序是,先從物件obj本身的屬性里找方法test,沒有找到,則參照屬性查找的發起者(即obj)所處類d的mro串列來依次檢索,首先在類d中未找到,然后再b中找到方法test)
2、繼承原理
【MRO】
python到底是如何實作繼承的呢?
對于你定義的每一個類,python都會計算出一個方法決議順序(mro)串列,該mro串列就是一個簡單的所有基類的線性順序串列,如下
>>> D.mro() # 新式類內置了mro方法可以查看線性串列的內容,經典類沒有該內置該方法 [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
python會在mro串列上從左到右開始查找基類,直到找到第一個匹配這個屬性的類為止,
而這個mro串列的構造是通過一個c3線性化演算法來實作的,
這個演算法實際上就是合并所有父類的mro串列并遵循如下三條準則:
1.子類會先于父類被檢查
2.多個父類會根據它們在串列中的順序被檢查
3.如果對下一個類存在兩個合法的選擇,選擇第一個父類
【PS】
1.由物件發起的屬性查找,會從物件自身的屬性里檢索,沒有則會按照物件的類.mro()規定的順序依次找下去
2.由類發起的屬性查找,會按照當前類.mro()規定的順序依次找下去
【舉個栗子】以上個“菱形問題”的栗子為基礎——
#【test1】 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(C,B): def test(self): print('from D') print(D.mro()) # 類D以及類D的物件訪問屬性都是參照該類的mro串列 # 輸出結果: # [<class '__main__.D'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>] obj1 = D() obj1.test() # 輸出結果:from D print(D.test) # 輸出結果:<function D.test at 0x0000012EA27FA4C0> print(C.mro()) # 類C以及類C的物件訪問屬性都是參照該類的mro串列 # 輸出結果:[<class '__main__.C'>, <class '__main__.A'>, <class 'object'>] obj2 = C() obj2.test() # 輸出結果:from C print(C.test) # 輸出結果: <function C.test at 0x0000022FA7D0A3A0> # 【test2】 class A(object): # def test(self): # print('from A') pass class B(A): def test(self): print('from B') class C(A): # def test(self): # print('from C') pass class D(C,B): # def test(self): # print('from D') pass print(D.mro()) # 類D以及類D的物件訪問屬性都是參照該類的mro串列 # 輸出結果: # [<class '__main__.D'>, <class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>] obj1 = D() obj1.test() # 輸出結果:from B print(D.test) # 輸出結果:<function B.test at 0x00000246D6F3A310> # 總結:類相關的屬性查找(類名.屬性,該類的物件.屬性),都是參照該類的mro【菱形問題+MRO】View Code
3、深度優先和廣度優先
(1)非菱形繼承
如果多繼承是非菱形繼承,經典類與新式類的屬性查找順序一樣:都是一個分支一個分支地找下去,然后最后找object類
==栗子如下:

class E: # def test(self): # print('from E') pass class F: 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') pass class D: def test(self): print('from D') class A(B, C, D): # def test(self): # print('from A') pass # 新式類 print(A.mro()) # A->B->E->C->F->D->object # [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, # <class '__main__.C'>, <class '__main__.F'>, <class '__main__.D'>, <class 'object'>] obj = A() obj.test() # 結果為:from F【非菱形的多繼承】View Code
(2)菱形繼承
如果多繼承是菱形繼承,經典類與新式類的屬性查找順序不一樣:
# 經典類:深度優先,會在檢索第一條分支的時候就直接一條道走到黑,即會檢索大腦袋(共同的父類)
# 新式類:廣度優先,會在檢索最后一條分支的時候檢索大腦袋
==栗子如下:
經典類:深度優先——

新式類:廣度優先——

class G: # 在python2中,未繼承object的類及其子類,都是經典類 # def test(self): # print('from G') pass class E(G): # def test(self): # print('from E') pass 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 # # 新式類 print(A.mro()) # # A->B->E->C->F->D->G->object # # [<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.F'>, # # <class '__main__.D'>, <class '__main__.G'>, <class 'object'>] obj = A() obj.test() # 輸出結果:from C # 經典類:A->B->E->G->C->F->D print(A.mro()) obj = A() obj.test() # 輸出結果:from C【菱形的多繼承】View Code
(3)總結
# 多繼承到底要不用?
要用,但是規避幾點問題——
# 1、繼承結構盡量不要過于復雜
# 2、推薦使用mixins機制:在多繼承的背景下滿足繼承的什么"是"什么的關系
4、python Mixins機制
五、派生與方法重用
六、組合
參考:
https://zhuanlan.zhihu.com/p/109331525
https://www.cnblogs.com/linhaifeng/articles/7340153.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/160770.html
標籤:Python
