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

微信公衆號:碼農充電站pro
我的主頁:https://codeshellme.github.iohtml

程序不是年輕的專利,可是,它屬於年輕。python

目錄git

在這裏插入圖片描述

咱們已經知道封裝繼承多態 是面向對象的三大特徵,面嚮對象語言都會提供這些機制。github

1,封裝

這一節介紹類的私有屬性和方法的時候,咱們已經講到過封裝shell

封裝就是在設計一個類的時候,只容許使用者訪問他須要的方法,將複雜的,沒有必要讓使用者知道的方法隱藏起來。這樣,使用者只需關注他須要的東西,爲其屏蔽了複雜性。微信

私有性就是實現封裝的一種手段,這樣,類的設計者就能夠控制類中的哪些屬性和方法能夠被使用者訪問到。通常,類中的屬性,和一些複雜的方法都不會暴露給使用者。函數

因爲前邊的章節介紹過封裝,這裏就再也不舉例說明了。ui

2,繼承

經過繼承的機制,可以使得子類輕鬆的擁有父類中的屬性和方法繼承也是一種代碼複用的方式。設計

Python 支持類的繼承,繼承的類叫作子類或者派生類被繼承的類叫作父類基類code

繼承的語法以下:

class 子類名(父類名):
    pass

子類名後邊的括號中,寫入要繼承的父類。

object

在Python 的繼承體系中,object 是最頂層類,它是全部類的父類。在定義一個類時,若是沒有繼承任何類,會默認繼承object 類。以下兩種定義方式是等價的:

# 沒有顯示繼承任何類,默認繼承 object
class A1:
    pass

# 顯示繼承 object
class A2(object):
    pass

每一個類中都有一個mro 方法,該方法能夠打印類的繼承關係(順序)。咱們來查看A1A2 的繼承關係:

>>> 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 繼承了AA 中有__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 方法,方法名相同,參數列表不一樣。

其實這種狀況在JavaC++ 是容許的,就是方法重載。而在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 繼承了ABA 中都有一個名爲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

ABC 中都有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() 方法時,而它繼承的父類 BC 中都有 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() 方法

CatDogDuck 都屬於動物,所以都繼承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() 函數,即便DrumAnimal 類絕不相關:

>>> 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 可以表現出鴨子特性的緣由。

(完。)


推薦閱讀:

Python 簡明教程 --- 16,Python 高階函數

Python 簡明教程 --- 17,Python 模塊與包

Python 簡明教程 --- 18,Python 面向對象

Python 簡明教程 --- 19,Python 類與對象

Python 簡明教程 --- 20,Python 類中的屬性與方法


歡迎關注做者公衆號,獲取更多技術乾貨。

碼農充電站pro

相關文章
相關標籤/搜索