程序不是年輕的專利如贷,但是,它屬于年輕捉偏。
目錄
我們已經(jīng)知道封裝
倒得,繼承
和多態(tài)
是面向?qū)ο蟮娜筇卣鳎嫦驅(qū)ο笳Z言都會提供這些機制夭禽。
1霞掺,封裝
在這一節(jié)介紹類的私有屬性和方法
的時候,我們已經(jīng)講到過封裝
讹躯。
封裝
就是在設(shè)計一個類的時候菩彬,只允許使用者訪問他需要的方法,將復雜的潮梯,沒有必要讓使用者知道的方法隱藏起來骗灶。這樣,使用者只需關(guān)注他需要的東西秉馏,為其屏蔽了復雜性耙旦。
私有性
就是實現(xiàn)封裝
的一種手段,這樣萝究,類的設(shè)計者就可以控制類中的哪些屬性和方法可以被使用者訪問到免都。一般,類中的屬性帆竹,和一些復雜的方法都不會暴露給使用者绕娘。
由于前邊的章節(jié)介紹過封裝,這里就不再舉例說明了栽连。
2险领,繼承
通過繼承
的機制侨舆,可使得子類
輕松的擁有父類
中的屬性和方法
。繼承
也是一種代碼復用
的方式绢陌。
Python 支持類的繼承挨下,繼承的類
叫做子類
或者派生類
,被繼承的類
叫做父類
或基類
下面。
繼承的語法如下:
class 子類名(父類名):
pass
在子類名
后邊的括號中复颈,寫入要繼承的父類。
object
類
在Python 的繼承體系中沥割,object
是最頂層類耗啦,它是所有類的父類。在定義一個類時机杜,如果沒有繼承任何類帜讲,會默認繼承object
類。如下兩種定義方式是等價的:
# 沒有顯示繼承任何類椒拗,默認繼承 object
class A1:
pass
# 顯示繼承 object
class A2(object):
pass
每個類中都有一個mro
方法似将,該方法可以打印類的繼承關(guān)系(順序)。我們來查看A1
和 A2
的繼承關(guān)系:
>>> A1.mro()
[<class '__main__.A1'>, <class 'object'>]
>>>
>>> A2.mro()
[<class '__main__.A2'>, <class 'object'>]
可見這兩個類都繼承了 object
類蚀苛。
繼承中的__init__
方法
當一個子類繼承一個父類時在验,如果子類中沒有定義__init__
,在創(chuàng)建子類的對象時堵未,會調(diào)用父類的__init__
方法腋舌,如下:
#! /usr/bin/env python3
class A(object):
def __init__(self):
print('A.__init__')
class B(A):
pass
以上代碼中,B
繼承了A
渗蟹,A
中有__init__
方法块饺,B
中沒有__init__
方法,創(chuàng)建類B
的對象b
:
>>> b = B()
A.__init__
可見A
中的__init__
被執(zhí)行了雌芽。
方法覆蓋
如果類B
中也定義了__init__
方法授艰,那么,就只會執(zhí)行B
中的__init__
方法世落,而不會執(zhí)行A
中的__init__
方法:
#! /usr/bin/env python3
class A(object):
def __init__(self):
print('A.__init__')
class B(A):
def __init__(self):
print('B.__init__')
此時創(chuàng)建B
的對象b
:
>>> b = B()
B.__init__
可見淮腾,此時只執(zhí)行了B
中的__init__
方法。這其實是方法覆蓋
的原因屉佳,因為子類
中的__init__
與父類
中的__init__
的參數(shù)列表一樣来破,此時,子類中的方法覆蓋了父類中的方法忘古,所以創(chuàng)建對象b
時,只會執(zhí)行B
中的__init__
方法诅诱。
當發(fā)生繼承關(guān)系(即一個子類繼承一個父類)時髓堪,如果子類中的一個方法與父類中的一個方法
一模一樣
(即方法名相同,參數(shù)列表也相同),這種情況就是方法覆蓋
(子類中的方法會覆蓋父類中的方法)干旁。
方法重載
當方法名
與參數(shù)列表
都一樣時會發(fā)生方法覆蓋
驶沼;當方法名
一樣,參數(shù)列表
不一樣時争群,會發(fā)生方法重載
回怜。
在單個類中,代碼如下:
#! /usr/bin/env python3
class A(object):
def __init__(self):
print('A.__init__')
def test(self):
print('test...')
def test(self, i):
print('test... i:%s' % i)
類A
中的兩個test
方法换薄,方法名
相同玉雾,參數(shù)列表
不同。
其實這種情況在Java
和 C++
是允許的轻要,就是方法重載
复旬。而在Python 中,雖然在類中這樣寫不會報錯冲泥,但實際上驹碍,下面的test(self, i)
已經(jīng)把上面的test(self)
給覆蓋掉了。創(chuàng)建出來的對象只能調(diào)用test(self, i)
凡恍,而test(self)
是不存在的志秃。
示例:
>>> a = A() # 創(chuàng)建 A 的對象 a
A.__init__
>>>
>>> a.test(123) # 可以調(diào)用 test(self, i) 方法
test... i:123
>>>
>>> a.test() # 調(diào)用 test(self) 發(fā)生異常
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: test() missing 1 required positional argument: 'i'
在繼承關(guān)系中,代碼如下:
#! /usr/bin/env python3
class A(object):
def __init__(self):
print('A.__init__')
def test(self):
print('test...')
class B(A):
def __init__(self):
print('B.__init__')
def test(self, i):
print('test... i:%s' % i)
上面代碼中B
繼承了A
嚼酝,B
和 A
中都有一個名為test
的方法浮还,但是參數(shù)列表
不同。
這種情況跟在單個類中的情況是一樣的革半,在類B
中碑定,test(self, i)
會覆蓋A 中的test(self)
,類B
的對象只能調(diào)用test(self, i)
又官,而不能調(diào)用test(self)
延刘。
示例:
>>> b = B() # 創(chuàng)建 B 的對象
B.__init__
>>>
>>> b.test(123) # 可以調(diào)用 test(self, i) 方法
test... i:123
>>>
>>> b.test() # 調(diào)用 test(self) 方法,出現(xiàn)異常
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: test() missing 1 required positional argument: 'i'
super()
方法
super()
方法用于調(diào)用父類中的方法六敬。
示例代碼:
#! /usr/bin/env python3
class A(object):
def __init__(self):
print('A.__init__')
def test(self):
print('class_A test...')
class B(A):
def __init__(self):
print('B.__init__')
super().__init__() # 調(diào)用父類中的構(gòu)造方法
def test(self, i):
print('class_B test... i:%s' % i)
super().test() # 調(diào)用父類中的 test 方法
演示:
>>> b = B() # 創(chuàng)建 B 的對象
B.__init__ # 調(diào)用 B 的構(gòu)造方法
A.__init__ # 調(diào)用 A 的構(gòu)造方法
>>>
>>> b.test(123) # 調(diào)用 B 中的 test 方法
class_B test... i:123
class_A test... # 執(zhí)行 A 中的 test 方法
is-a
關(guān)系
一個子類的對象碘赖,同時也是一個父類的對象,這叫做is-a
關(guān)系外构。但是一個父類的對象普泡,不一定是一個子類的對象。
這很好理解审编,就像撼班,貓一定是動物,但動物不一定是貓垒酬。
我們可以使用isinstance()
函數(shù)來判斷一個對象是否是一個類的實例砰嘁。
比如我們有如下兩個類件炉,Cat
繼承了 Animal
:
#! /usr/bin/env python3
class Animal(object):
pass
class Cat(Animal):
pass
來看下對象和類之間的從屬關(guān)系:
>>> a = Animal() # 創(chuàng)建 Animal 的對象
>>> c = Cat() # 創(chuàng)建 Cat 的對象
>>>
>>> isinstance(a, Animal) # a 一定是 Animal 的實例
True
>>> isinstance(c, Cat) # c 一定是 Cat 的實例
True
>>>
>>> isinstance(c, Animal) # Cat 繼承了 Animal,所以 c 也是 Animal 的實例
True
>>> isinstance(a, Cat) # 但 a 不是 Cat 的實例
False
3矮湘,多繼承
多繼承
就是一個子類同時繼承多個父類斟冕,這樣,這個子類就同時擁有了多個父類的特性缅阳。
C++ 語言中允許多繼承磕蛇,但由于多繼承會使得類的繼承關(guān)系變得復雜。因此十办,到了Java 中秀撇,就禁止了多繼承的方式,取而代之的是橘洞,在Java 中允許同時繼承多個接口
捌袜。
Python 中也允許多繼承,語法如下:
# 括號中可以寫多個父類
class 子類名(父類1, 父類2, ...):
pass
我們構(gòu)造一個如下的繼承關(guān)系:
代碼如下:
#! /usr/bin/env python3
class A(object):
def test(self):
print('class_A test...')
class B(A):
def test(self):
print('class_B test...')
class C(A):
def test(self):
print('class_C test...')
class D(B, C):
pass
類A
炸枣,B
虏等,C
中都有test()
方法,D
中沒有test()
方法适肠。
使用D
類中的mro()
方法查看繼承關(guān)系:
>>> D.mro()
[<class 'Test.D'>, <class 'Test.B'>, <class 'Test.C'>, <class 'Test.A'>, <class 'object'>]
創(chuàng)建D
的對象:
>>> d = D()
如果類D
中有test()
方法霍衫,那么d.test()
肯定會調(diào)用D
中的test()
方法,這種情況很簡單侯养,不用多說敦跌。
當類D
中沒有test()
方法時,而它繼承的父類 B
和 C
中都有 test()
方法逛揩,此時會調(diào)用哪個test()
呢柠傍?
>>> d.test()
class_B test...
可以看到d.test()
調(diào)用了類B
中的 test()
方法。
實際上這種情況下辩稽,Python 解釋器會根據(jù)D.mro()
的輸出結(jié)果來依次查找test()
方法惧笛,即查找順序是D->B->C->A->object
。
所以d.test()
調(diào)用了類B
中的 test()
方法逞泄。
建議:
由于
多繼承
會使類的繼承關(guān)系變得復雜患整,所以并不提倡過多的使用多繼承
。
4喷众,多態(tài)
多態(tài)
從字面上理解就是一個事物可以呈現(xiàn)多種狀態(tài)各谚。繼承
是多態(tài)的基礎(chǔ)。
在上面的例子中到千,類D
的對象d
調(diào)用test()
方法時昌渤,沿著繼承鏈
(D.mro()
)查找合適的test()
方法的過程,就是多態(tài)的表現(xiàn)過程憔四。
比如愈涩,我們有以下幾個類:
-
Animal
:有一個speak()
方法 -
Cat
:繼承Animal
類望抽,有自己的speak()
方法 -
Dog
:繼承Animal
類,有自己的speak()
方法 -
Duck
:繼承Animal
類履婉,有自己的speak()
方法
Cat
,Dog
斟览,Duck
都屬于動物毁腿,因此都繼承Animal
,代碼如下:
#! /usr/bin/env python3
class Animal(object):
def speak(self):
print('動物會說話...')
class Cat(Animal):
def speak(self):
print('喵喵...')
class Dog(Animal):
def speak(self):
print('汪汪...')
class Duck(Animal):
def speak(self):
print('嘎嘎...')
def animal_speak(animal):
animal.speak()
我們還定義了一個animal_speak
函數(shù)苛茂,它接受一個參數(shù)animal
已烤,在函數(shù)內(nèi),調(diào)用了speak()
方法妓羊。
實際上胯究,這種情況下,我們調(diào)用animal_speak
函數(shù)時躁绸,可以為它傳遞Animal
類型的對象裕循,以及任何的Animal
子類的對象。
傳遞Animal
的對象時净刮,調(diào)用了Animal
類中的 speak()
:
>>> animal_speak(Animal())
動物會說話...
傳遞Cat
的對象時剥哑,調(diào)用了Cat
類中的 speak()
:
>>> animal_speak(Cat())
喵喵...
傳遞Dog
的對象時,調(diào)用了Dog
類中的 speak()
:
>>> animal_speak(Dog())
汪汪...
傳遞Duck
的對象時淹父,調(diào)用了Duck
類中的 speak()
:
>>> animal_speak(Duck())
嘎嘎...
可以看到株婴,我們可以給animal_speak()
函數(shù)傳遞多種不同類型
的對象,為animal_speak()
函數(shù)傳遞不同類型的參數(shù)暑认,輸出了不同的結(jié)果困介,這就是多態(tài)
。
5蘸际,鴨子類型
在靜態(tài)類型
語言中座哩,有嚴格的類型判斷,上面的animal_speak()
函數(shù)的參數(shù)只能傳遞Animal
及其子類
的對象捡鱼。
而Python 屬于動態(tài)類型
語言八回,不會進行嚴格的類型判斷。
因此驾诈,我們不僅可以為animal_speak()
函數(shù)傳遞Animal
及其子類
的對象缠诅,還可以傳遞其它與Animal
類毫不相關(guān)的類的對象,只要該類中有speak()
方法就行乍迄。
這種特性管引,在Python 中被叫做鴨子類型
,意思就是闯两,只要一個事物走起來像鴨子褥伴,叫起來像鴨子谅将,那么它就是鴨子,即使它不是真正的鴨子
重慢。
從代碼上來說饥臂,只要一個類中有speak()
方法,那么就可以將該類的對象傳遞給animal_speak()
函數(shù)似踱。
比如隅熙,有一個鼓類Drum
,其中有一個函數(shù)speak()
:
class Drum(object):
def speak(self):
print('咚咚...')
那么核芽,類Drum
的對象也可以傳遞給animal_speak()
函數(shù)囚戚,即使Drum
與Animal
類毫不相關(guān):
>>> animal_speak(Drum())
咚咚...
從另一個角度來考慮,實際上Python 函數(shù)中的參數(shù)轧简,并沒有標明參數(shù)的類型驰坊。在animal_speak()
函數(shù)中,我們只是將參數(shù)叫做了animal
而已哮独,因此我們就認為animal_speak()
函數(shù)應該接受Animal 類及其子類的對象拳芙,其實這僅僅只是我們認為的而已。
計算機并不知道animal
的含義借嗽,如果我們將原來的animal_speak()
函數(shù):
def animal_speak(animal):
animal.speak()
改寫成:
def animal_speak(a):
a.speak()
實際上态鳖,我們知道,這兩個函數(shù)并沒有任何區(qū)別恶导。因此浆竭,參數(shù)a
可以是任意的類型,只要a
中有speak()
方法就行惨寿。這就是Python 能夠表現(xiàn)出鴨子特性
的原因邦泄。
(完。)
推薦閱讀:
Python 簡明教程 ---16裂垦,Python 高階函數(shù)
Python 簡明教程 ---17顺囊,Python 模塊與包
Python 簡明教程 ---18,Python 面向?qū)ο?/a>
Python 簡明教程 ---19蕉拢,Python 類與對象
Python 簡明教程 ---20害幅,Python 類中的屬性與方法