面向?qū)ο笈c繼承
面向?qū)ο笏枷胗腥笠兀?/p>
- 繼承
- 封裝
- 多態(tài)
面向?qū)ο缶幊蹋∣OP)語(yǔ)言的一個(gè)重要功能就是 “繼承”:
- 它可以使用現(xiàn)有類(lèi)的所有功能,并在無(wú)需重新編寫(xiě)原來(lái)類(lèi)的情況下,對(duì)這些功能進(jìn)行擴(kuò)展
- 通過(guò)繼承創(chuàng)建的新類(lèi)被稱(chēng)為 “子類(lèi)” 或 “派生類(lèi)”,被繼承的類(lèi)被稱(chēng)為 “基類(lèi)”、“父類(lèi)” 或 “超類(lèi)”
- 在 Python/ target=_blank class=infotextkey>Python 中,同時(shí)支持單繼承與多繼承
繼承的概念
舉個(gè)例子,我們現(xiàn)在像創(chuàng)建豬、狗和貓三個(gè)類(lèi),它們都有名字和年齡屬性,也都有一個(gè)叫的方法。不同的是,豬有吃的方法、狗有看家的方法、貓有抓老鼠的方法。按照之前的學(xué)習(xí),我們會(huì)將代碼寫(xiě)成這樣:
class Pig:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
print('叫')
def eat(self):
print('吃')
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
print('叫')
def guarding(self):
print('看家')
class Cat:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
print('叫')
def catch(self):
print('抓老鼠')
我們發(fā)現(xiàn),雖然實(shí)現(xiàn)了需求,但是我們看到,這里面出現(xiàn)了大量的重復(fù)代碼。如果我們能將這些重復(fù)代碼封裝起來(lái),比如封裝到一個(gè)動(dòng)物類(lèi)中,然后豬、狗和貓分別都繼承這個(gè)動(dòng)物類(lèi),就可以讓代碼更加簡(jiǎn)潔。
具體的實(shí)現(xiàn)方法為:
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
print('叫')
class Pig(Animal):
def eat(self):
print('吃')
class Dog(Animal):
def guarding(self):
print('看家')
class Cat(Animal):
def catch(self):
print('抓老鼠')
mimi = Cat('咪咪', 3)
print(mimi.name, mimi.age)
mimi.bark()
mimi.catch()
輸出的結(jié)果為:
咪咪 3
叫
抓老鼠
實(shí)現(xiàn)繼承之后,子類(lèi)將繼承父類(lèi)的屬性和方法。
不難看出,繼承關(guān)系的特點(diǎn)為:
- 增加了類(lèi)的耦合性(耦合性不宜多,宜精)
- 減少了重復(fù)代碼
- 使得代碼更加規(guī)范化,合理化
組合與繼承的對(duì)比:
- 組合
- 組合是指在新類(lèi)里面創(chuàng)建原有類(lèi)的對(duì)象,重復(fù)利用已有類(lèi)的功能,是 has-a 的關(guān)系(如:貓有腿)
- 原來(lái)類(lèi)的對(duì)象作為整體,以新類(lèi)的屬性的形式存在
- 繼承
- 繼承允許設(shè)計(jì)人員根據(jù)其他類(lèi)的實(shí)現(xiàn)來(lái)定義一個(gè)類(lèi)的實(shí)現(xiàn),是 is-a 的關(guān)系(如:貓是動(dòng)物)
- 子類(lèi)可以直接使用父類(lèi)中的屬性和方法,就好像父類(lèi)的屬性和方法已經(jīng)存在于子類(lèi)中了一樣
Python 3 中使用的都是新式類(lèi),如果一個(gè)類(lèi)誰(shuí)都不繼承,那么它默認(rèn)繼承 object 類(lèi)。
繼承雖然很好用,但是不能濫用,像之前說(shuō)的,耦合程度不宜過(guò)高,否則邏輯會(huì)十分混亂:
- 不要輕易地使用繼承,除非兩個(gè)類(lèi)之間是 is-a 關(guān)系
- 不要單純地為了實(shí)現(xiàn)代碼的重用而使用繼承,因?yàn)檫^(guò)多的繼承會(huì)破壞代碼的可維護(hù)性,當(dāng)父類(lèi)被修改的時(shí)候,會(huì)影響到所有繼承自它的子類(lèi),從而增加程序的維護(hù)難度與成本
- 總結(jié)起來(lái)就是:組裝的時(shí)候使用組合,擴(kuò)展的時(shí)候使用繼承
回到我們剛才的例子,豬、狗、貓三各類(lèi)都只有動(dòng)物一個(gè)父類(lèi),這種只有一個(gè)父類(lèi)的繼承方式,我們稱(chēng)作為單繼承。在單繼承中,子類(lèi)可以繼承父類(lèi)的屬性和方法,修改父類(lèi),所有子類(lèi)都會(huì)受到影響。
isinstance和issubclass
isinstance:
- 用于檢查實(shí)例類(lèi)型
- isinstance(對(duì)象, 類(lèi)),用來(lái)判斷對(duì)象是不是該類(lèi)的實(shí)例對(duì)象
issubclass:
- 用于檢查類(lèi)繼承
- issubclass(類(lèi)1, 類(lèi)2),用來(lái)判斷類(lèi) 1 是否是類(lèi) 2 的子類(lèi)
類(lèi)與數(shù)據(jù)類(lèi)型
Python 與其他編程語(yǔ)言不同,當(dāng)我們定義一個(gè) class 的時(shí)候,我們實(shí)際上就定義了一個(gè)數(shù)據(jù)類(lèi)型。我們定義的數(shù)據(jù)類(lèi)型和 Python 自帶的數(shù)據(jù)類(lèi)型,比如 str、list、dict 沒(méi)什么兩樣:
print(isinstance(10, int))
輸出的結(jié)果為: True
重寫(xiě)父類(lèi)方法
如果父類(lèi)中的方法在子類(lèi)中不適用,我們可以對(duì)其進(jìn)行重寫(xiě):
class Animal:
def __init__(self, name, age):
self.name = name
self.age = age
def bark(self):
print('叫')
class Dog(Animal):
def bark(self): # 重寫(xiě)叫的方法
print('汪汪汪!')
def guarding(self):
print('看家')
wangwang = Dog('汪汪', 3)
print(wangwang.name)
wangwang.bark()
輸出的結(jié)果為:
汪汪
汪汪汪!
重寫(xiě)父類(lèi)方法的原理是,當(dāng)示例調(diào)用方法時(shí),會(huì)先在自己的類(lèi)方法中查找,如果找不到,才會(huì)去父類(lèi)中查找是否有相應(yīng)的方法。如果在自己的類(lèi)方法中找到了需要的方法,就不會(huì)去父類(lèi)中查找,也就調(diào)用不到父類(lèi)的同名方法,從而實(shí)現(xiàn)對(duì)父類(lèi)中方法的重寫(xiě)
調(diào)用父類(lèi)方法
但是有些時(shí)候,我們不得已會(huì)寫(xiě)一些重名的方法,比如父類(lèi)和子類(lèi)都會(huì)有 __init__ 構(gòu)造方法。但是我們?cè)谡{(diào)用子類(lèi)方法的同時(shí),也希望調(diào)用到父類(lèi)中相應(yīng)的方法。我們可以通過(guò)父類(lèi)的類(lèi)名直接調(diào)用:
class Father:
eye_num = 2
def __init__(self, name, age):
self.name = name
self.age = age
def live_like_yemen(self):
print('打兒子')
class Son(Father):
hair_color = '藍(lán)色'
def __init__(self, name, age, sex):
Father.__init__(self, name, age)
self.sex = sex
def live_like_yemen(self):
print('打弟弟')
xiaoming = Son('小明', 16, '男')
xiaoming.live_like_yemen()
print(xiaoming.name)
輸出的結(jié)果為:
打弟弟
小明
需要注意的是,在類(lèi)中,self 永遠(yuǎn)指的是調(diào)用類(lèi)的實(shí)例化對(duì)象。
super 方法
在上面的例子中,如果沒(méi)有 Father.__init__(self, name, age) 這行代碼,在子類(lèi)中就無(wú)法調(diào)用父類(lèi)的構(gòu)造方法,因?yàn)樽宇?lèi)已經(jīng)重寫(xiě)了構(gòu)造方法。上面的方法雖然實(shí)現(xiàn)了預(yù)期的功能,但是并不符合開(kāi)發(fā)規(guī)范。
從子類(lèi)中,調(diào)用父類(lèi)中方法的關(guān)鍵字是 super,上述例子可修改為:
class Father:
eye_num = 2
def __init__(self, name, age):
self.name = name
self.age = age
def live_like_yemen(self):
print('打兒子')
class Son(Father):
hair_color = '藍(lán)色'
def __init__(self, name, age, sex):
super().__init__(name, age) # 也可以寫(xiě)為super(Son, self).__init__(name, age)
self.sex = sex
def live_like_yemen(self):
print('打弟弟')
xiaoming = Son('小明', 16, '男')
xiaoming.live_like_yemen()
print(xiaoming.name)
super 方法:
- 子類(lèi)如果編寫(xiě)了自己的構(gòu)造方法,但是沒(méi)有聲明要調(diào)用父類(lèi)的構(gòu)造方法,而還需要父類(lèi)的構(gòu)造函數(shù)中初始化的一些屬性,就會(huì)出現(xiàn)問(wèn)題
- 如果子類(lèi)和父類(lèi)都有構(gòu)造函數(shù),子類(lèi)的構(gòu)造函數(shù)其實(shí)是對(duì)父類(lèi)的構(gòu)造函數(shù)的重寫(xiě)。如果不顯示調(diào)用父類(lèi)構(gòu)造函數(shù),父類(lèi)的構(gòu)造函數(shù)便不會(huì)被執(zhí)行
- 解決方法:直接使用超類(lèi)的類(lèi)名調(diào)用超類(lèi)構(gòu)造方法,或者使用 super 函數(shù) super(當(dāng)前類(lèi)名, self).__init__()
父類(lèi)方法重寫(xiě):
- 子類(lèi)可以重寫(xiě)父類(lèi)中的方法
- 通過(guò) super 關(guān)鍵字可以調(diào)用父類(lèi)中的方法
Python 中的多繼承
多重繼承和多繼承
多重繼承:包含多個(gè)間接父類(lèi)
class A(object): pass
class B(A): pass
class C(B): pass
多繼承:有多個(gè)直接父類(lèi)
class X(object): pass
class Y(object): pass
class Z(object): pass
class M(X, Y, Z): pass
大部分面向?qū)ο蟮木幊陶Z(yǔ)言(除了 C++)都只支持單繼承,而不支持多繼承
- 多繼承不僅增加了編程的復(fù)雜度,而且很容易導(dǎo)致一些莫名的錯(cuò)誤 ^1
Python 雖然在語(yǔ)法上明確支持多繼承,但通常推薦如果不是很有必要,盡量不要使用多繼承,而是使用單繼承
- 這樣可以保證編程思路更清晰,而且可以避免很多麻煩
如果多個(gè)直接父類(lèi)中包含了同名的方法
- 排在前面的父類(lèi)中的方法會(huì) “遮蔽 “排在后面的父類(lèi)中的同名方法
class A:
def method(self):
print('A_method')
class B:
def method(self):
print('B_method')
class C(A, B):
pass
c = C()
c.method()
輸出的結(jié)果為:
A_method
鉆石繼承和 MRO
我們剛剛談到,即便不使用 super 方法,直接使用父類(lèi)的類(lèi)名,同樣可以實(shí)現(xiàn)對(duì)父類(lèi)方法的調(diào)用。那為什么更推薦使用 super 方法呢?
這是因?yàn)楫?dāng)涉及到比較復(fù)雜得多繼承關(guān)系,比如鉆石繼承關(guān)系時(shí),會(huì)出現(xiàn)間接父類(lèi)會(huì)被初始化多次的情況。
比如,我們來(lái)看下面這個(gè)鉆石繼承的例子,如果我們使用父類(lèi)的類(lèi)名調(diào)用構(gòu)造方法:
class YeYe:
def __init__(self):
print('初始化爺爺類(lèi)')
class QinBa(YeYe):
def __init__(self):
print('進(jìn)入化親爸類(lèi)')
YeYe.__init__(self)
print('初始化親爸類(lèi)')
class GanDie(YeYe):
def __init__(self):
print('進(jìn)入化干爹類(lèi)')
YeYe.__init__(self)
print('初始化干爹類(lèi)')
class ErZi(QinBa, GanDie):
def __init__(self):
print('進(jìn)入化兒子類(lèi)')
QinBa.__init__(self)
GanDie.__init__(self)
print('初始化兒子類(lèi)')
erzi = ErZi()
我們看到,程序運(yùn)行后,爺爺類(lèi)被初始化了兩次。
這是因?yàn)椋?dāng)創(chuàng)建兒子對(duì)象時(shí),會(huì)執(zhí)行它的構(gòu)造函數(shù)。首先打印的是兒子類(lèi)中初始化方法的代碼,然后執(zhí)行秦霸的構(gòu)造方法。在親爸的構(gòu)造方法中,也是先打印代碼,然后執(zhí)行爺爺?shù)臉?gòu)造方法。執(zhí)行完?duì)敔數(shù)臉?gòu)造方法之后,程序繼續(xù)執(zhí)行親爸中剩余的代碼,然后回到兒子類(lèi)中,執(zhí)行干爹的構(gòu)造方法。在干爹的構(gòu)造方法中,又要調(diào)用爺爺?shù)臉?gòu)造方法。然后打印剩余代碼,直至結(jié)束。
我們看到,第五步和第十步都是要調(diào)用爺爺?shù)臉?gòu)造方法,爺爺類(lèi)被初始化了兩次。這種情況一來(lái)沒(méi)有必要,會(huì)占用很大空間,二來(lái),多次初始化也會(huì)帶來(lái)程序邏輯的混亂。
如果我們改用 super 函數(shù)來(lái)進(jìn)行這樣的操作,就不會(huì)有這些麻煩:
class YeYe:
def __init__(self):
print('初始化爺爺類(lèi)')
class QinBa(YeYe):
def __init__(self):
print('進(jìn)入親爸類(lèi)')
super().__init__()
print('初始化親爸類(lèi)')
class GanDie(YeYe):
def __init__(self):
print('進(jìn)入干爹類(lèi)')
super().__init__()
print('初始化干爹類(lèi)')
class ErZi(QinBa, GanDie):
def __init__(self):
print('進(jìn)入兒子類(lèi)')
super().__init__()
print('初始化兒子類(lèi)')
erzi = ErZi()
首先,我們發(fā)現(xiàn),在兒子類(lèi)中,我們只用一行代碼指代調(diào)用兩個(gè)直接父類(lèi)的構(gòu)造方法。然后,從結(jié)果上看,此時(shí),爺爺類(lèi)只被初始化一次。
而且我們發(fā)現(xiàn),代碼的運(yùn)行情況與多個(gè)裝飾器裝飾一個(gè)函數(shù)的情況很類(lèi)似,子類(lèi)的代碼包含著父類(lèi)的代碼,一層套一層的形式。
查看 mro 的方法有兩種:
類(lèi)名.mro()
對(duì)象名.__class__.mro()
前面例子中的 mro 為:
[<class '__main__.ErZi'>, <class '__main__.QinBa'>, <class '__main__.GanDie'>, <class '__main__.YeYe'>, <class 'object'>]
我們說(shuō)過(guò),super 后面什么都不寫(xiě),默認(rèn)和 super(當(dāng)前類(lèi)名, self) 的寫(xiě)法一樣。但事實(shí)上,super 的參數(shù)除了可以寫(xiě)當(dāng)前類(lèi)名外,還可以寫(xiě)它的父類(lèi) [^3] 的類(lèi)名。此時(shí),會(huì)執(zhí)行在方法解析順序列表中,該類(lèi)下一個(gè)類(lèi)的方法。
補(bǔ)充了這些知識(shí),我們就可以解釋上面的程序運(yùn)行的順序了。
super 關(guān)鍵字詳解:
- 基本結(jié)構(gòu):super(class[, object or class])
- Python 3 可以直接使用 super().xxx 代替 super(class, self).xxx
- 使用多繼承時(shí),會(huì)涉及到查找順序(MRO)、鉆石繼承等問(wèn)題單繼承時(shí),類(lèi)名.__init__() 的方式和 super().__init__() 的方式調(diào)用父類(lèi)中的方法沒(méi)有什么差別使用 類(lèi)名.__init__() 的方式在鉆石繼承時(shí),會(huì)遇到初始化混亂的問(wèn)題
super 內(nèi)核的 mro 方法:返回的是一個(gè)類(lèi)的方法解析順序表(順序結(jié)構(gòu))
- 我們定義的每一個(gè)類(lèi),Python 都會(huì)計(jì)算出一個(gè)方法解析順序(MRO [^2])列表這也是 super 在父類(lèi)中查找成員的順序,它是通過(guò) C3 線性算法來(lái)實(shí)現(xiàn)的
- 每個(gè)父類(lèi) [^3] 都存在且只在其中出現(xiàn)一次
- 我們可以通過(guò)下面兩種方式獲得某個(gè)類(lèi)的 mro 列表:
類(lèi)名.mro()
對(duì)象名.__class__.mro()
- 當(dāng)使用 super(cls, obj) 時(shí),Python 會(huì)在 obj 的 mro 列表上搜索 cls 的下一個(gè)類(lèi)
事實(shí)上,super 和父類(lèi)沒(méi)有實(shí)質(zhì)性的關(guān)聯(lián),我們也不一定非要把 super 后面的參數(shù)寫(xiě)成自己類(lèi)的名字和 self。我們甚至可以很靈活地給 super 傳參數(shù)
super(cls, obj) 獲得的是 cls 在 obj 的 MRO 列表中的下一個(gè)類(lèi),cls 可以是任何一個(gè)類(lèi),obj 可以是任何一個(gè)對(duì)象,只要合理即可
class class ErZi(Qinba,GanDie):
def __init__(self):
super(ErZi, self).__init__()
print('初始化兒子')
在前面我們定義兒子類(lèi)的時(shí)候,如果我們不想調(diào)用親爸的 __init__(),而是要調(diào)用干爹的 __init__(),只需把 super 寫(xiě)成 super(Qinba, self).__init__(),也就是這樣:
class YeYe:
def __init__(self):
print('初始化爺爺類(lèi)')
class QinBa(YeYe):
def __init__(self):
print('進(jìn)入親爸類(lèi)')
super().__init__()
print('初始化親爸類(lèi)')
class GanDie(YeYe):
def __init__(self):
print('進(jìn)入干爹類(lèi)')
super().__init__()
print('初始化干爹類(lèi)')
class ErZi(QinBa, GanDie):
def __init__(self):
print('進(jìn)入兒子類(lèi)')
super(QinBa, self).__init__()
print('初始化兒子類(lèi)')
erzi = ErZi()
其執(zhí)行順序?yàn)椋?/p>
[^2]: Method Resolution Order
[^3]: 包括直接父類(lèi)和間接父類(lèi)