Python 簡明教程 --- 21,Python 繼承與多型
阿新 • • 發佈:2020-07-04
> **微信公眾號:碼農充電站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