文章目錄
一、繼承介紹
繼承是一種創建新類的方式,在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,也會默認繼承該類。
# 什麼是object類?
提示:object類提供了一些常用內置方法的實現,如用來在打印對象時返回字符串的內置方法__str__
二、繼承與抽象
要找出類與類之間的繼承關係,需要先抽象,再繼承。抽象即總結相似之處,總結對象之間的相似之處得到類,總結類與類之間的相似之處就可以得到父類,如下圖所示
基於抽象的結果,我們就找到了繼承關係
子類可以繼承/遺傳父類所有的屬性,因而繼承可以用來解決類與類之間的代碼重用性問題
。比如我們按照定義Student類的方式再定義一個Teacher類
class People:
school='清華大學'
def __init__(self,name,sex,age):
self.name=name
self.sex=sex
self.age=age
class Student(People): # 括號裏是表示繼承的父類爲People
def choose(self):
print('%s is choosing a course' %self.name)
class Teacher(People): # 括號裏是表示繼承的父類爲People
def __init__(self,name,sex,age,teach_lesson):
People.__init__(self,name,sex,age) # 相同屬性交給父類初始化
self.teach_lesson = teach_lesson # 不同屬性自己初始化
def teach(self):
print('%s is teaching' %self.name)
# Student類和teacher類有部分同樣的屬性,因此我們可以通過繼承提高代碼重用!
teacher1=Teacher('lili','male',18,'數學')
print(teacher1.__dict__)
# 運行結果:
{'name': 'lili', 'sex': 'male', 'age': 18, 'teach_lesson': '數學'}
三、屬性查找
有了繼承關係,對象在查找屬性時,先從對象自己的__dict__中找,如果沒有則去子類中找,然後再去父類中找……
class Foo:
def f1(self):
print('Foo.f1')
def f2(self):
print('Foo.f2')
self.f1() # 如果我想要程序運行到這裏,執行的是Foo類的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()方法,是在父類中找到的f1方法
# 運行結果:
Foo.f2
Foo.f1
四、繼承的實現原理
1、菱形問題
大多數面嚮對象語言都不支持多繼承,而在Python中,一個子類是可以同時繼承多個父類的,這固然可以帶來一個子類可以對多個不同父類加以重用的好處,但多繼承也有可能引發著名的Diamond problem菱形問題
(或稱鑽石問題,有時候也被稱爲“死亡鑽石”),菱形其實就是對下面這種繼承結構的形象比喻
# 注意A如果是Object內置類,不算是菱形。
這種繼承結構下導致的問題稱之爲菱形問題:如果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的繼承實現原理
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. 如果對下一個類存在兩個合法的選擇,選擇第一個父類
查找流程:
1.由對象發起的屬性查找,會從對象自身的屬性裏檢索,沒有則會按照對象的類.mro()規定的順序依次找下去,
2.由類發起的屬性查找,會按照當前類.mro()規定的順序依次找下去,
3、多繼承屬性查詢順序(不查詢MRO,看出查詢順序)
首先我們要知道,不論python2還是python3的新式類都是一個東西,一樣的搜索順序(造成區別的是菱形與非菱形)
,只是python2需要class haha(object):
表示新式類。python3默認繼承object
,只有新式類而已。
(1)多繼承結構爲非菱形結構
此時,會按照先找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
obj = A()
obj.test()
# 重點預警:
1. 如果在python2中,上述全爲經典類,搜索路線爲:A->B->E->C->F->D,經典類由於沒有MRO查看方法,因此,只能試。
2. 如果在python3中,全部默認繼承object內置類,全爲新式類,搜索路線爲:A->B->E->C->F->D->object
3. 不論菱形還是非菱形新式類中的object內置類總是最後搜索
(2)多繼承結構爲菱形結構
如果繼承關係爲菱形結構,那麼經典類與新式類會有不同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() # 如上圖,查找順序爲:A->B->E->G->C->F->D
# 可依次註釋上述類中的方法test來進行驗證,注意請在python2.x中進行測試
------------------------------------ 新式類 --------------------------------------
下面我直接用python2中的新式類來講,python3搜索順序也是一樣的,只是多加個object而已。
注意看下圖的箭頭的標號,表示了搜索順序:
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() # 如上圖,查找順序爲:A->B->E->C->F->D->G->object (上面就說過,新式類中的object無論菱形還是非菱形,都是最後搜索)
# 可依次註釋上述類中的方法test來進行驗證
4、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”關係。這個難題該怎麼解決?
答:
1. 這個時候就需要Mixins機制,這個機制使在儘量減少代碼冗餘的情況下,且遵守了'is-a'這種繼承思想,且使多繼承不再複雜,代碼變得清晰!
2. Mixins機制是一種規範,不是一種方法,例如:常量的定義(全爲大寫即爲常量,但實際還是變量,遵守命名規範和使用規範,命名規範即:全爲大寫爲常量,使用規範即:看到全爲大寫的變量不要去改它的值)
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
# Mixins機制解讀:汽車、直升飛機、民航飛機都是交通工具(is-a關係),但三者不是飛行器,Flyable只是加給(混入)民航飛機、直升飛機的功能。這就有點裝飾器的味道了。這樣既實現了多繼承,又增強了可閱讀性,而且保護了is-a的關係
可以看到,上面的CivilAircraft、Helicopter類實現了多繼承,不過它繼承的第一個類我們起名爲FlyableMixin,而不是Flyable,這個並不影響功能,但是會告訴後來讀代碼的人,這個類是一個Mixin類,表示混入(mix-in),這種命名方式就是用來明確地告訴別人(python語言慣用的手法),這個類是作爲功能添加到子類中,而不是作爲父類,它的作用同Java中的接口
。所以從含義上理解,CivilAircraft、Helicopter類都只是一個Vehicle,而不是一個飛行器。
# 使用Mixin類實現多重繼承要非常小心
1. Mixin類在多繼承寫的時候應該在最左邊一次寫需要繼承的Mixin類名,is-a的父類應該在最右邊
2. 首先它必須表示某一種功能,而不是某個物品,python 對於mixin類的命名方式一般以 Mixin, able, ible 爲後綴
3. 其次它必須責任單一,如果有多種功能(例如有關飛行功能寫一個,有關鑽),那就寫多個Mixin類,一個類可以繼承多個Mixin,爲了保證遵循繼承的“is-a”原則,只能繼承一個標識其歸屬含義的父類
4. 然後,它不依賴於子類的實現,這個有點類似裝飾器,與被裝飾函數無關,這個Mixins類在編寫的時候,應該儘量和子類無關,就像這裏的飛行器,我把這個飛行器給個,哪個就能飛,並不是針對某個類設計的
5. 最後,子類即便沒有繼承這個Mixin類,也照樣可以工作,就是缺少了某個功能。(比如飛機照樣可以載客,就是不能飛了)
Mixins是從多個類中重用代碼的好方法,但是需要付出相應的代價,我們定義的Minx類越多,子類的代碼可讀性就會越差
,並且更噁心的是,在繼承的層級變多時,代碼閱讀者在定位某一個方法到底在何處調用時會暈頭轉向。
5、java對多繼承的處理方法
// 抽象基類:交通工具類
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 {
}
// 接口可以多實現,相當於Mixin類.只不過java的接口在定義的時候,不允許實現,接口依賴於子類的實現.
五、派生與方法重用
1、引出問題
相對於父類,子類可以派生出自己新的屬性,如下,相對於人,教師有自己的級別和薪水兩個新屬性。
>>> 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,level,salary): # 派生
... self.name=name
... self.sex=sex
... self.age=age
... self.level=level
... self.salary=salary
... def teach(self):
... print('%s is teaching' %self.name)
...
>>> obj=Teacher('lili','female',28,'高級講師',3000) #只會找自己類中的__init__,並不會自動調用父類的
>>> obj.name,obj.sex,obj.age,obj.level,obj.salary
('lili', 'female', 28, '高級講師',3000)
很明顯子類Teacher中__init__內的前三行又是在寫重複代碼,若想在子類派生出的方法內重用父類的功能,有兩種實現方式。
2、方式一:指名道姓
... def __init__(self,name,sex,age,title):
... People.__init__(self,name,age,sex) # 類調用的函數,因而需要傳入self
... self.level=level
... self.salary=salary
... def teach(self):
... print('%s is teaching' %self.name)
本篇博客的第二大點,就用到了這個方法。
3、 方式二: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()爲準)super會找到A類的父類即是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,'博士生導師',1990,3,23)
# teacher1有兩門課程
teacher1.courses.append(python)
teacher1.courses.append(linux)
# 重用Date類的功能
teacher1.birth.tell_birth()
# 重用Course類的功能
for obj in teacher1.courses:
obj.tell_info()
以上述代碼爲例,teacher類中傳入的是Date類的對象和Course類的對象有什麼好處呢?
答:可以更方便的使用Date類、Course類中的屬性,提高代碼的重用性