Python 簡明教程 --- 21,Python 繼承與多態
微信公衆號:碼農充電站pro
個人主頁: https://codeshellme.github.io
程序不是年輕的專利,但是,它屬於年輕。
目錄
我們已經知道 封裝
, 繼承
和 多態
是面向對象的三大特徵,面嚮對象語言都會提供這些機制。
1,封裝
在這一節介紹類的 私有屬性和方法
的時候,我們已經講到過 封裝
。
封裝
就是在設計一個類的時候,只允許使用者訪問他需要的方法,將複雜的,沒有必要讓使用者知道的方法隱藏起來。這樣,使用者只需關注他需要的東西,爲其屏蔽了複雜性。
私有性
就是實現 封裝
的一種手段,這樣,類的設計者就可以控制類中的哪些屬性和方法可以被使用者訪問到。一般,類中的屬性,和一些複雜的方法都不會暴露給使用者。
由於前邊的章節介紹過封裝,這裏就不再舉例說明了。
2,繼承
通過 繼承
的機制,可使得 子類
輕鬆的擁有 父類
中的 屬性和方法
。 繼承
也是一種 代碼複用
的方式。
Python 支持類的繼承, 繼承的類
叫做 子類
或者 派生類
, 被繼承的類
叫做 父類
或 基類
。
繼承的語法如下:
class 子類名(父類名): pass
在 子類名
後邊的括號中,寫入要繼承的父類。
object
類
在Python 的繼承體系中, object
是最頂層類,它是所有類的父類。在定義一個類時,如果沒有繼承任何類,會默認繼承 object
類。如下兩種定義方式是等價的:
# 沒有顯示繼承任何類,默認繼承 object class A1: pass # 顯示繼承 object class A2(object): pass
每個類中都有一個 mro
方法,該方法可以打印類的繼承關係(順序)。我們來查看 A1
和 A2
的繼承關係:
>>> A1.mro() [<class '__main__.A1'>, <class 'object'>] >>> >>> A2.mro() [<class '__main__.A2'>, <class 'object'>]
可見這兩個類都繼承了 object
類。
繼承中的 __init__
方法
當一個子類繼承一個父類時,如果子類中沒有定義 __init__
,在創建子類的對象時,會調用父類的 __init__
方法,如下:
#! /usr/bin/env python3 class A(object): def __init__(self): print('A.__init__') class B(A): pass
以上代碼中, B
繼承了 A
, A
中有 __init__
方法, B
中沒有 __init__
方法,創建類 B
的對象 b
:
>>> b = B() A.__init__
可見 A
中的 __init__
被執行了。
方法覆蓋
如果類 B
中也定義了 __init__
方法,那麼,就只會執行 B
中的 __init__
方法,而不會執行 A
中的 __init__
方法:
#! /usr/bin/env python3 class A(object): def __init__(self): print('A.__init__') class B(A): def __init__(self): print('B.__init__')
此時創建 B
的對象 b
:
>>> b = B() B.__init__
可見,此時只執行了 B
中的 __init__
方法。這其實是 方法覆蓋
的原因,因爲 子類
中的 __init__
與 父類
中的 __init__
的參數列表一樣,此時,子類中的方法覆蓋了父類中的方法,所以創建對象 b
時,只會執行 B
中的 __init__
方法。
當發生繼承關係(即一個子類繼承一個父類)時,如果子類中的一個方法與父類中的一個方法 一模一樣
(即方法名相同,參數列表也相同),這種情況就是 方法覆蓋
(子類中的方法會覆蓋父類中的方法)。
方法重載
當 方法名
與 參數列表
都一樣時會發生 方法覆蓋
;當 方法名
一樣, 參數列表
不一樣時,會發生 方法重載
。
在單個類中,代碼如下:
#! /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
方法, 方法名
相同, 參數列表
不同。
其實這種情況在 Java
和 C++
是允許的,就是 方法重載
。而在Python 中,雖然在類中這樣寫不會報錯,但實際上,下面的 test(self, i)
已經把上面的 test(self)
給覆蓋掉了。創建出來的對象只能調用 test(self, i)
,而 test(self)
是不存在的。
示例:
>>> a = A() # 創建 A 的對象 a A.__init__ >>> >>> a.test(123) # 可以調用 test(self, i) 方法 test... i:123 >>> >>> a.test() # 調用 test(self) 發生異常 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: test() missing 1 required positional argument: 'i'
在繼承關係中,代碼如下:
#! /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
的方法,但是 參數列表
不同。
這種情況跟在單個類中的情況是一樣的,在類 B
中, test(self, i)
會覆蓋A 中的 test(self)
,類 B
的對象只能調用 test(self, i)
,而不能調用 test(self)
。
示例:
>>> b = B() # 創建 B 的對象 B.__init__ >>> >>> b.test(123) # 可以調用 test(self, i) 方法 test... i:123 >>> >>> b.test() # 調用 test(self) 方法,出現異常 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: test() missing 1 required positional argument: 'i'
super()
方法
super()
方法用於調用父類中的方法。
示例代碼:
#! /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__() # 調用父類中的構造方法 def test(self, i): print('class_B test... i:%s' % i) super().test() # 調用父類中的 test 方法
演示:
>>> b = B() # 創建 B 的對象 B.__init__ # 調用 B 的構造方法 A.__init__ # 調用 A 的構造方法 >>> >>> b.test(123) # 調用 B 中的 test 方法 class_B test... i:123 class_A test... # 執行 A 中的 test 方法
is-a
關係
一個子類的對象,同時也是一個父類的對象,這叫做 is-a
關係。但是一個父類的對象,不一定是一個子類的對象。
這很好理解,就像,貓一定是動物,但動物不一定是貓。
我們可以使用 isinstance()
函數來判斷一個對象是否是一個類的實例。
比如我們有如下兩個類, Cat
繼承了 Animal
:
#! /usr/bin/env python3 class Animal(object): pass class Cat(Animal): pass
來看下對象和類之間的從屬關係:
>>> a = Animal() # 創建 Animal 的對象 >>> c = Cat() # 創建 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++ 語言中允許多繼承,但由於多繼承會使得類的繼承關係變得複雜。因此,到了Java 中,就禁止了多繼承的方式,取而代之的是,在Java 中允許同時繼承多個 接口
。
Python 中也允許多繼承,語法如下:
# 括號中可以寫多個父類 class 子類名(父類1, 父類2, ...): pass
我們構造一個如下的繼承關係:
代碼如下:
#! /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()
方法查看繼承關係:
>>> D.mro() [<class 'Test.D'>, <class 'Test.B'>, <class 'Test.C'>, <class 'Test.A'>, <class 'object'>]
創建 D
的對象:
>>> d = D()
如果類 D
中有 test()
方法,那麼 d.test()
肯定會調用 D
中的 test()
方法,這種情況很簡單,不用多說。
當類 D
中沒有 test()
方法時,而它繼承的父類 B
和 C
中都有 test()
方法,此時會調用哪個 test()
呢?
>>> d.test() class_B test...
可以看到 d.test()
調用了類 B
中的 test()
方法。
實際上這種情況下,Python 解釋器會根據 D.mro()
的輸出結果來依次查找 test()
方法,即查找順序是 D->B->C->A->object
。
所以 d.test()
調用了類 B
中的 test()
方法。
建議:
由於 多繼承
會使類的繼承關係變得複雜,所以並不提倡過多的使用 多繼承
。
4,多態
多態
從字面上理解就是一個事物可以呈現多種狀態。 繼承
是多態的基礎。
在上面的例子中,類 D
的對象 d
調用 test()
方法時,沿着 繼承鏈
( D.mro()
)查找合適的 test()
方法的過程,就是多態的表現過程。
比如,我們有以下幾個類:
-
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
函數,它接受一個參數 animal
,在函數內,調用了 speak()
方法。
實際上,這種情況下,我們調用 animal_speak
函數時,可以爲它傳遞 Animal
類型的對象,以及任何的 Animal
子類的對象。
傳遞 Animal
的對象時,調用了 Animal
類中的 speak()
:
>>> animal_speak(Animal()) 動物會說話...
傳遞 Cat
的對象時,調用了 Cat
類中的 speak()
:
>>> animal_speak(Cat()) 喵喵...
傳遞 Dog
的對象時,調用了 Dog
類中的 speak()
:
>>> animal_speak(Dog()) 汪汪...
傳遞 Duck
的對象時,調用了 Duck
類中的 speak()
:
>>> animal_speak(Duck()) 嘎嘎...
可以看到,我們可以給 animal_speak()
函數傳遞 多種不同類型
的對象,爲 animal_speak()
函數傳遞不同類型的參數,輸出了不同的結果,這就是 多態
。
5,鴨子類型
在 靜態類型
語言中,有嚴格的類型判斷,上面的 animal_speak()
函數的參數只能傳遞 Animal
及其 子類
的對象。
而Python 屬於 動態類型
語言,不會進行嚴格的類型判斷。
因此,我們不僅可以爲 animal_speak()
函數傳遞 Animal
及其 子類
的對象,還可以傳遞其它與 Animal
類毫不相關的類的對象,只要該類中有 speak()
方法就行。
這種特性,在Python 中被叫做 鴨子類型
,意思就是, 只要一個事物走起來像鴨子,叫起來像鴨子,那麼它就是鴨子,即使它不是真正的鴨子
。
從代碼上來說,只要一個類中有 speak()
方法,那麼就可以將該類的對象傳遞給 animal_speak()
函數。
比如,有一個鼓類 Drum
,其中有一個函數 speak()
:
class Drum(object): def speak(self): print('咚咚...')
那麼,類 Drum
的對象也可以傳遞給 animal_speak()
函數,即使 Drum
與 Animal
類毫不相關:
>>> animal_speak(Drum()) 咚咚...
從另一個角度來考慮,實際上Python 函數中的參數,並沒有標明參數的類型。在 animal_speak()
函數中,我們只是將參數叫做了 animal
而已,因此我們就認爲 animal_speak()
函數應該接受Animal 類及其子類的對象,其實這僅僅只是我們認爲的而已。
計算機並不知道 animal
的含義,如果我們將原來的 animal_speak()
函數:
def animal_speak(animal): animal.speak()
改寫成:
def animal_speak(a): a.speak()
實際上,我們知道,這兩個函數並沒有任何區別。因此,參數 a
可以是任意的類型,只要 a
中有 speak()
方法就行。這就是Python 能夠表現出 鴨子特性
的原因。
(完。)