目錄:
- 繼承介紹
- 繼承與抽象
- 屬性查找
- 繼承的實現原理
- 菱形問題
- 繼承原理
- 深度優先和廣度優先
- python Mixins機制
- 派生與方法重用
- 組合
- 視頻鏈接
一 繼承介紹
繼承是一種創建新類的方式,在Python中,新建的類可以繼承一個或多個父類,新建的類可稱為子類或派生類,父類又可稱為基類或超類
class ParentClass1: #定義父類 pass class ParentClass2: #定義父類 pass class SubClass1(ParentClass1): #單繼承 pass class SubClass2(ParentClass1,ParentClass2): #多繼承 pass
通過類的內置屬性__bases__可以查看類繼承的所有父類
>>> SubClass2.__bases__ (<class '__main__.ParentClass1'>, <class '__main__.ParentClass2'>)
在Python2中有經典類與新式類之分,沒有顯式地繼承object類的類,以及該類的子類,都是經典類,顯式地繼承object的類,以及該類的子類,都是新式類。而在Python3中,即使沒有顯式地繼承object,也會默認繼承該類,如下
>>> ParentClass1.__bases__ (<class ‘object'>,) >>> ParentClass2.__bases__ (<class 'object'>,)
因而在Python3中統一都是新式類,關於經典類與新式類的區別,我們稍后討論
提示:object類提供了一些常用內置方法的實現,如用來在打印對象時返回字符串的內置方法__str__
二 繼承與抽象
要找出類與類之間的繼承關系,需要先抽象,再繼承。抽象即總結相似之處,總結對象之間的相似之處得到類,總結類與類之間的相似之處就可以得到父類,如下圖所示
基於抽象的結果,我們就找到了繼承關系
基於上圖我們可以看出類與類之間的繼承指的是什么’是’什么的關系(比如人類,豬類,猴類都是動物類)。子類可以繼承/遺傳父類所有的屬性,因而繼承可以用來解決類與類之間的代碼重用性問題。比如我們按照定義Student類的方式再定義一個Teacher類
class Teacher: school='清華大學' def __init__(self,name,sex,age): self.name=name self.sex=sex self.age=age def teach(self): print('%s is teaching' %self.name)
類Teacher與Student之間存在重復的代碼,老師與學生都是人類,所以我們可以得出如下繼承關系,實現代碼重用
class People: school='清華大學' def __init__(self,name,sex,age): self.name=name self.sex=sex self.age=age class Student(People): def choose(self): print('%s is choosing a course' %self.name) class Teacher(People): def teach(self): print('%s is teaching' %self.name)
Teacher類內並沒有定義__init__方法,但是會從父類中找到__init__,因而仍然可以正常實例化,如下
>>> teacher1=Teacher('lili','male',18) >>> teacher1.school,teacher1.name,teacher1.sex,teacher1.age ('清華大學', 'lili', 'male', 18)
三 屬性查找
有了繼承關系,對象在查找屬性時,先從對象自己的__dict__中找,如果沒有則去子類中找,然后再去父類中找……
>>> class Foo: ... def f1(self): ... print('Foo.f1') ... def f2(self): ... print('Foo.f2') ... self.f1() ... >>> class Bar(Foo): ... def f1(self): ... print('Foo.f1') ... >>> b=Bar() >>> b.f2() Foo.f2 Foo.f1
b.f2()會在父類Foo中找到f2,先打印Foo.f2,然后執行到self.f1(),即b.f1(),仍會按照:對象本身->類Bar->父類Foo的順序依次找下去,在類Bar中找到f1,因而打印結果為Foo.f1
父類如果不想讓子類覆蓋自己的方法,可以采用雙下划線開頭的方式將方法設置為私有的
>>> class Foo: ... def __f1(self): # 變形為_Foo__fa ... print('Foo.f1') ... def f2(self): ... print('Foo.f2') ... self.__f1() # 變形為self._Foo__fa,因而只會調用自己所在的類中的方法 ... >>> class Bar(Foo): ... def __f1(self): # 變形為_Bar__f1 ... print('Foo.f1') ... >>> >>> b=Bar() >>> b.f2() #在父類中找到f2方法,進而調用b._Foo__f1()方法,同樣是在父類中找到該方法 Foo.f2 Foo.f1
四 繼承的實現原理
4.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的繼承實現原理
4.2 繼承原理
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.如果對下一個類存在兩個合法的選擇,選擇第一個父類
所以obj.test()的查找順序是,先從對象obj本身的屬性里找方法test,沒有找到,則參照屬性查找的發起者(即obj)所處類D的MRO列表來依次檢索,首先在類D中未找到,然后再B中找到方法test
ps:
1.由對象發起的屬性查找,會從對象自身的屬性里檢索,沒有則會按照對象的類.mro()規定的順序依次找下去, 2.由類發起的屬性查找,會按照當前類.mro()規定的順序依次找下去,
4.3 深度優先和廣度優先
參照下述代碼,多繼承結構為非菱形結構,此時,會按照先找B這一條分支,然后再找C這一條分支,最后找D這一條分支的順序直到找到我們想要的屬性
class E: def test(self): print('from E') class F: def test(self): print('from F') class B(E): def test(self): print('from B') class C(F): def test(self): print('from C') class D: def test(self): print('from D') class A(B, C, D): # def test(self): # print('from A') pass print(A.mro()) ''' [<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 B # 可依次注釋上述類中的方法test來進行驗證
如果繼承關系為菱形結構,那么經典類與新式類會有不同MRO,分別對應屬性的兩種查找方式:深度優先和廣度優先
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') 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() obj.test() # 如上圖,查找順序為:obj->A->B->E->G->C->F->D->object # 可依次注釋上述類中的方法test來進行驗證,注意請在python2.x中進行測試
class G(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') 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() obj.test() # 如上圖,查找順序為:obj->A->B->E->C->F->D->G->object # 可依次注釋上述類中的方法test來進行驗證
4.4 Pyton Mixins機制
一個子類可以同時繼承多個父類,這樣的設計常被人詬病,一來它有可能導致可惡的菱形問題,二來在人的世界觀里繼承應該是個”is-a”關系。 比如轎車類之所以可以繼承交通工具類,是因為基於人的世界觀,我們可以說:轎車是一個(“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
但是如果民航飛機和直升機都各自寫自己的飛行fly方法,又違背了代碼盡可能重用的原則(如果以后飛行工具越來越多,那會重復代碼將會越來越多)。
怎么辦???為了盡可能地重用代碼,那就只好在定義出一個飛行器的類,然后讓民航飛機和直升飛機同時繼承交通工具以及飛行器兩個父類,這樣就出現了多重繼承。這時又違背了繼承必須是”is-a”關系。這個難題該怎么解決?
不同的語言給出了不同的方法,讓我們先來了解Java的處理方法。Java提供了接口interface功能,來實現多重繼承:
// 抽象基類:交通工具類 public abstract class Vehicle { } // 接口:飛行器 public interface Flyable { public void fly(); } // 類:實現了飛行器接口的類,在該類中實現具體的fly方法,這樣下面民航飛機與直升飛機在實現fly時直接重用即可 public class FlyableImpl implements Flyable { public void fly() { System.out.println("I am flying"); } } // 民航飛機,繼承自交通工具類,並實現了飛行器接口 public class CivilAircraft extends Vehicle implements Flyable { private Flyable flyable; public CivilAircraft() { flyable = new FlyableImpl(); } public void fly() { flyable.fly(); } } // 直升飛機,繼承自交通工具類,並實現了飛行器接口 public class Helicopter extends Vehicle implements Flyable { private Flyable flyable; public Helicopter() { flyable = new FlyableImpl(); } public void fly() { flyable.fly(); } } // 汽車,繼承自交通工具類, public class Car extends Vehicle { }
現在我們的飛機同時具有了交通工具及飛行器兩種屬性,而且我們不需要重寫飛行器中的飛行方法,同時我們沒有破壞單一繼承的原則。飛機就是一種交通工具,可飛行的能力是飛機的屬性,通過繼承接口來獲取。
回到主題,Python語言可沒有接口功能,但Python提供了Mixins機制,簡單來說Mixins機制指的是子類混合(mixin)不同類的功能,而這些類采用統一的命名規范(例如Mixin后綴),以此標識這些類只是用來混合功能的,並不是用來標識子類的從屬"is-a"關系的,所以Mixins機制本質仍是多繼承,但同樣遵守”is-a”關系,如下
class Vehicle: # 交通工具 pass class FlyableMixin: def fly(self): ''' 飛行功能相應的代碼 ''' print("I am flying") class CivilAircraft(FlyableMixin, Vehicle): # 民航飛機 pass class Helicopter(FlyableMixin, Vehicle): # 直升飛機 pass class Car(Vehicle): # 汽車 pass # ps: 采用某種規范(如命名規范)來解決具體的問題是python慣用的套路
可以看到,上面的CivilAircraft、Helicopter類實現了多繼承,不過它繼承的第一個類我們起名為FlyableMixin,而不是Flyable,這個並不影響功能,但是會告訴后來讀代碼的人,這個類是一個Mixin類,表示混入(mix-in),這種命名方式就是用來明確地告訴別人(python語言慣用的手法),這個類是作為功能添加到子類中,而不是作為父類,它的作用同Java中的接口。所以從含義上理解,CivilAircraft、Helicopter類都只是一個Vehicle,而不是一個飛行器。
使用Mixin類實現多重繼承要非常小心
- 首先它必須表示某一種功能,而不是某個物品,python 對於mixin類的命名方式一般以 Mixin, able, ible 為后綴
- 其次它必須責任單一,如果有多個功能,那就寫多個Mixin類,一個類可以繼承多個Mixin,為了保證遵循繼承的“is-a”原則,只能繼承一個標識其歸屬含義的父類
- 然后,它不依賴於子類的實現
- 最后,子類即便沒有繼承這個Mixin類,也照樣可以工作,就是缺少了某個功能。(比如飛機照樣可以載客,就是不能飛了)
Mixins是從多個類中重用代碼的好方法,但是需要付出相應的代價,我們定義的Minx類越多,子類的代碼可讀性就會越差,並且更惡心的是,在繼承的層級變多時,代碼閱讀者在定位某一個方法到底在何處調用時會暈頭轉向,如下
class Displayer: def display(self, message): print(message) class LoggerMixin: def log(self, message, filename='logfile.txt'): with open(filename, 'a') as fh: fh.write(message) def display(self, message): super().display(message) # super的用法請參考下一小節 self.log(message) class MySubClass(LoggerMixin, Displayer): def log(self, message): super().log(message, filename='subclasslog.txt') obj = MySubClass() obj.display("This string will be shown and logged in subclasslog.txt") # 屬性查找的發起者是obj,所以會參照類MySubClass的MRO來檢索屬性 #[<class '__main__.MySubClass'>, <class '__main__.LoggerMixin'>, <class '__main__.Displayer'>, <class 'object'>] # 1、首先會去對象obj的類MySubClass找方法display,沒有則去類LoggerMixin中找,找到開始執行代碼 # 2、執行LoggerMixin的第一行代碼:執行super().display(message),參照MySubClass.mro(),super會去下一個類即類Displayer中找,找到display,開始執行代碼,打印消息"This string will be shown and logged in subclasslog.txt" # 3、執行LoggerMixin的第二行代碼:self.log(message),self是對象obj,即obj.log(message),屬性查找的發起者為obj,所以會按照其類MySubClass.mro(),即MySubClass->LoggerMixin->Displayer->object的順序查找,在MySubClass中找到方法log,開始執行super().log(message, filename='subclasslog.txt'),super會按照MySubClass.mro()查找下一個類,在類LoggerMixin中找到log方法開始執行,最終將日志寫入文件subclasslog.txt
ps:課外了解小知識
Java只允許接口的多重繼承。接口本質上是抽象基類,具有所有抽象方法,沒有數據成員。 與java一樣,python也有抽象類的概念但是同樣需要借助模塊實現,抽象類是一個特殊的類,它的特殊之處在於只能被繼承,不能被實例化,繼承的子類必須實現抽象基類規定的方法,這樣便可保證始終只有一個特定方法或屬性的實現,並且不會產生歧義,因而也可以起到避免菱形問題的作用 java的interface:https://www.cnblogs.com/linhaifeng/articles/7340153.html#_label6 python的抽象基類:https://www.cnblogs.com/linhaifeng/articles/7340153.html#_label7
五 派生與方法重用
子類可以派生出自己新的屬性,在進行屬性查找時,子類中的屬性名會優先於父類被查找,例如每個老師還有職稱這一屬性,我們就需要在Teacher類中定義該類自己的__init__覆蓋父類的
>>> class People: ... school='清華大學' ... ... def __init__(self,name,sex,age): ... self.name=name ... self.sex=sex ... self.age=age ... >>> class Teacher(People): ... def __init__(self,name,sex,age,title): # 派生 ... self.name=name ... self.sex=sex ... self.age=age ... self.title=title ... def teach(self): ... print('%s is teaching' %self.name) ... >>> obj=Teacher('lili','female',28,'高級講師') #只會找自己類中的__init__,並不會自動調用父類的 >>> obj.name,obj.sex,obj.age,obj.title ('lili', 'female', 28, '高級講師')
很明顯子類Teacher中__init__內的前三行又是在寫重復代碼,若想在子類派生出的方法內重用父類的功能,有兩種實現方式
方法一:“指名道姓”地調用某一個類的函數
>>> class Teacher(People): ... def __init__(self,name,sex,age,title): ... People.__init__(self,name,age,sex) #調用的是函數,因而需要傳入self ... self.title=title ... def teach(self): ... print('%s is teaching' %self.name) ...
方法二:super()
調用super()會得到一個特殊的對象,該對象專門用來引用父類的屬性,且嚴格按照MRO規定的順序向后查找
>>> class Teacher(People): ... def __init__(self,name,sex,age,title): ... super().__init__(name,age,sex) #調用的是綁定方法,自動傳入self ... self.title=title ... def teach(self): ... print('%s is teaching' %self.name) ...
提示:在Python2中super的使用需要完整地寫成super(自己的類名,self) ,而在python3中可以簡寫為super()。
這兩種方式的區別是:方式一是跟繼承沒有關系的,而方式二的super()是依賴於繼承的,並且即使沒有直接繼承關系,super()仍然會按照MRO繼續往后查找
>>> #A沒有繼承B ... class A: ... def test(self): ... super().test() ... >>> class B: ... def test(self): ... print('from B') ... >>> class C(A,B): ... pass ... >>> C.mro() # 在代碼層面A並不是B的子類,但從MRO列表來看,屬性查找時,就是按照順序C->A->B->object,B就相當於A的“父類” [<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,<class ‘object'>] >>> obj=C() >>> obj.test() # 屬性查找的發起者是類C的對象obj,所以中途發生的屬性查找都是參照C.mro() from B
obj.test()首先找到A下的test方法,執行super().test()會基於MRO列表(以C.mro()為准)當前所處的位置繼續往后查找(),然后在B中找到了test方法並執行。
關於在子類中重用父類功能的這兩種方式,使用任何一種都可以,但是在最新的代碼中還是推薦使用super()
六 組合
在一個類中以另外一個類的對象作為數據屬性,稱為類的組合。組合與繼承都是用來解決代碼的重用性問題。不同的是:繼承是一種“是”的關系,比如老師是人、學生是人,當類之間有很多相同的之處,應該使用繼承;而組合則是一種“有”的關系,比如老師有生日,老師有多門課程,當類之間有顯著不同,並且較小的類是較大的類所需要的組件時,應該使用組合,如下示例
class Course: def __init__(self,name,period,price): self.name=name self.period=period self.price=price def tell_info(self): print('<%s %s %s>' %(self.name,self.period,self.price)) class Date: def __init__(self,year,mon,day): self.year=year self.mon=mon self.day=day def tell_birth(self): print('<%s-%s-%s>' %(self.year,self.mon,self.day)) class People: school='清華大學' def __init__(self,name,sex,age): self.name=name self.sex=sex self.age=age #Teacher類基於繼承來重用People的代碼,基於組合來重用Date類和Course類的代碼 class Teacher(People): #老師是人 def __init__(self,name,sex,age,title,year,mon,day): super().__init__(name,age,sex) self.birth=Date(year,mon,day) #老師有生日 self.courses=[] #老師有課程,可以在實例化后,往該列表中添加Course類的對象 def teach(self): print('%s is teaching' %self.name) python=Course('python','3mons',3000.0) linux=Course('linux','5mons',5000.0) teacher1=Teacher('lili','female',28