1. 程式人生 > >Python 簡明教程 --- 21,Python 繼承與多型

Python 簡明教程 --- 21,Python 繼承與多型

> **微信公眾號:碼農充電站pro** > **個人主頁:** > 程式不是年輕的專利,但是,它屬於年輕。 **目錄** ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200627103603632.png?#pic_center) 我們已經知道`封裝`,`繼承`和`多型` 是面向物件的三大特徵,面嚮物件語言都會提供這些機制。 ### 1,封裝 在[這一節](https://www.cnblogs.com/codeshell/p/13197968.html)介紹類的`私有屬性和方法`的時候,我們已經講到過`封裝`。 `封裝`就是在設計一個類的時候,只允許使用者訪問他需要的方法,將複雜的,沒有必要讓使用者知道的方法隱藏起來。這樣,使用者只需關注他需要的東西,為其遮蔽了複雜性。 `私有性`就是實現`封裝`的一種手段,這樣,類的設計者就可以控制類中的哪些屬性和方法可以被使用者訪問到。一般,類中的屬性,和一些複雜的方法都不會暴露給使用者。 由於[前邊的章節](https://www.cnblogs.com/codeshell/p/13197968.html)介紹過封裝,這裡就不再舉例說明了。 ### 2,繼承 通過`繼承`的機制,可使得`子類`輕鬆的擁有`父類`中的`屬性和方法`。`繼承`也是一種`程式碼複用`的方式。 Python 支援類的繼承,`繼承的類`叫做`子類`或者`派生類`,`被繼承的類`叫做`父類`或`基類`。 繼承的語法如下: ```python class 子類名(父類名): pass ``` 在`子類名`後邊的括號中,寫入要繼承的父類。 **`object` 類** 在Python 的繼承體系中,`object` 是最頂層類,它是所有類的父類。在定義一個類時,如果沒有繼承任何類,會預設繼承`object` 類。如下兩種定義方式是等價的: ```python # 沒有顯示繼承任何類,預設繼承 object class A1: pass # 顯示繼承 object class A2(object): pass ``` 每個類中都有一個`mro` 方法,該方法可以列印類的繼承關係(順序)。我們來檢視`A1` 和 `A2` 的繼承關係: ```shell >
>> A1.mro() [, ] >>> >>> A2.mro() [, ] ``` 可見這兩個類都繼承了 `object` 類。 **繼承中的`__init__` 方法** 當一個子類繼承一個父類時,如果子類中沒有定義`__init__`,在建立子類的物件時,會呼叫父類的`__init__` 方法,如下: ```python #! /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`: ```shell >>> b = B() A.__init__ ``` 可見`A` 中的`__init__` 被執行了。 **方法覆蓋** 如果類`B` 中也定義了`__init__` 方法,那麼,就只會執行`B` 中的`__init__` 方法,而不會執行`A` 中的`__init__` 方法: ```python #! /usr/bin/env python3 class A(object): def __init__(self): print('A.__init__') class B(A): def __init__(self): print('B.__init__') ``` 此時建立`B` 的物件`b`: ```shell >>> b = B() B.__init__ ``` 可見,此時只執行了`B` 中的`__init__` 方法。這其實是`方法覆蓋`的原因,因為`子類`中的`__init__` 與`父類`中的`__init__` 的引數列表一樣,此時,子類中的方法覆蓋了父類中的方法,所以建立物件`b` 時,只會執行`B` 中的`__init__` 方法。 > 當發生繼承關係(即一個子類繼承一個父類)時,如果子類中的一個方法與父類中的一個方法`一模一樣`(即方法名相同,引數列表也相同),這種情況就是`方法覆蓋`(子類中的方法會覆蓋父類中的方法)。 **方法過載** 當`方法名`與`引數列表`都一樣時會發生`方法覆蓋`;當`方法名`一樣,`引數列表`不一樣時,會發生`方法過載`。 在單個類中,程式碼如下: ```python #! /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)` 是不存在的。 示例: ```shell >>> 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 "", line 1, in TypeError: test() missing 1 required positional argument: 'i' ``` 在繼承關係中,程式碼如下: ```python #! /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)`。 示例: ```shell >>> b = B() # 建立 B 的物件 B.__init__ >>> >>> b.test(123) # 可以呼叫 test(self, i) 方法 test... i:123 >>> >>> b.test() # 呼叫 test(self) 方法,出現異常 Traceback (most recent call last): File "", line 1, in TypeError: test() missing 1 required positional argument: 'i' ``` **`super()` 方法** `super()` 方法用於呼叫父類中的方法。 示例程式碼: ```python #! /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 方法 ``` 演示: ```shell >>> 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`: ```python #! /usr/bin/env python3 class Animal(object): pass class Cat(Animal): pass ``` 來看下物件和類之間的從屬關係: ```shell >>> 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 中也允許多繼承,語法如下: ```python # 括號中可以寫多個父類 class 子類名(父類1, 父類2, ...): pass ``` 我們構造一個如下的繼承關係: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200626175140508.png?#pic_center) 程式碼如下: ```python #! /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()`方法檢視繼承關係: ```shell >>> D.mro() [, , , , ] ``` 建立`D` 的物件: ```shell >>> d = D() ``` 如果類`D` 中有`test()` 方法,那麼`d.test()` 肯定會呼叫`D` 中的`test()` 方法,這種情況很簡單,不用多說。 當類`D` 中沒有`test()` 方法時,而它繼承的父類 `B` 和 `C` 中都有 `test()` 方法,此時會呼叫哪個`test()` 呢? ```shell >>> 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`,程式碼如下: ```python #! /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()`: ```shell >>> animal_speak(Animal()) 動物會說話... ``` 傳遞`Cat` 的物件時,呼叫了`Cat` 類中的 `speak()`: ```shell >>> animal_speak(Cat()) 喵喵... ``` 傳遞`Dog` 的物件時,呼叫了`Dog` 類中的 `speak()`: ```shell >>> animal_speak(Dog()) 汪汪... ``` 傳遞`Duck` 的物件時,呼叫了`Duck` 類中的 `speak()`: ```shell >>> animal_speak(Duck()) 嘎嘎... ``` 可以看到,我們可以給`animal_speak()` 函式傳遞`多種不同型別`的物件,為`animal_speak()` 函式傳遞不同型別的引數,輸出了不同的結果,這就是`多型`。 ### 5,鴨子型別 在`靜態型別`語言中,有嚴格的型別判斷,上面的`animal_speak()` 函式的引數只能傳遞`Animal` 及其`子類`的物件。 而Python 屬於`動態型別`語言,不會進行嚴格的型別判斷。 因此,我們不僅可以為`animal_speak()` 函式傳遞`Animal` 及其`子類`的物件,還可以傳遞其它與`Animal` 類毫不相關的類的物件,只要該類中有`speak()` 方法就行。 這種特性,在Python 中被叫做`鴨子型別`,意思就是,`只要一個事物走起來像鴨子,叫起來像鴨子,那麼它就是鴨子,即使它不是真正的鴨子`。 從程式碼上來說,只要一個類中有`speak()` 方法,那麼就可以將該類的物件傳遞給`animal_speak()` 函式。 比如,有一個鼓類`Drum`,其中有一個函式`speak()`: ```python class Drum(object): def speak(self): print('咚咚...') ``` 那麼,類`Drum` 的物件也可以傳遞給`animal_speak()` 函式,即使`Drum` 與`Animal` 類毫不相關: ```shell >>> animal_speak(Drum()) 咚咚... ``` 從另一個角度來考慮,實際上Python 函式中的引數,並沒有標明引數的型別。在`animal_speak()` 函式中,我們只是將引數叫做了`animal` 而已,因此我們就認為`animal_speak()` 函式應該接受Animal 類及其子類的物件,其實這僅僅只是我們認為的而已。 計算機並不知道`animal` 的含義,如果我們將原來的`animal_speak()` 函式: ```python def animal_speak(animal): animal.speak() ``` 改寫成: ```python def animal_speak(a): a.speak() ``` 實際上,我們知道,這兩個函式並沒有任何區別。因此,引數`a`可以是任意的型別,只要`a` 中有`speak()` 方法就行。這就是Python 能夠表現出`鴨子特性`的原因。 (完。) --- **推薦閱讀:** [Python 簡明教程 --- 16,Python 高階函式](https://www.cnblogs.com/codeshell/p/13158903.html) [Python 簡明教程 --- 17,Python 模組與包](https://www.cnblogs.com/codeshell/p/13158924.html) [Python 簡明教程 --- 18,Python 面向物件](https://www.cnblogs.com/codeshell/p/13193851.html) [Python 簡明教程 --- 19,Python 類與物件](https://www.cnblogs.com/codeshell/p/13193866.html) [Python 簡明教程 --- 20,Python 類中的屬性與方法](https://www.cnblogs.com/codeshell/p/13197968.html) --- 歡迎關注作者公眾號,獲取更多技術乾貨。 ![碼農充電站pro](https://img-blog.csdnimg.cn/20200505082843773.png?#pic