Python 入門指南 5.0

單元 15 - 繼承與多型

~~學習進度表~~

物件導向程式設計 (object-oriented programming) 有三大基本特性,分別是封裝 (encapsulation) 、繼承 (inheritance) 及多型 (polymorphism)

Encapsulation
    Inheritance
        Polymorphism

繼承的目的是讓類別 (class) 具有像是親屬的垂直關係(父母子女),子類別 (subclass) 可以擁有父類別 (superclass) 的成員 (member) ,而多型像是親屬的平行關係(兄弟姊妹),多個子類別繼承自單一父類別之時,這些子類別就可以用父類別代替,父類別如同家族裡的「姓」,子類別則是「名」。

繼承的英文動詞原文 inherit ,中文意思泛指從什麼得到什麼,生物學上的遺傳也是用這個詞,其實中文講「繼承」不太貼切,因為講到中文的「繼承」,通常是指長輩去世,然後晚輩才繼承長輩的財產之類的。事實上在物件導向中,父類別、子類別所建立的物件在程式執行期間都是同時存在,並不構成父類別會不存在的問題,但由於早期譯者將 inheritance 翻譯成「繼承」,這裡是依習慣沿用「繼承」一詞。

我們已經在單元 13 介紹過封裝。

用最簡單的話來解釋,繼承就是讓子類別可以使用父類別的屬性 (attribute) 及方法 (method) ,也就是建立子類別物件 (object) 之時,會同時建立父類別的物件,當子類別物件使用父類別屬性或方法,就會連結到父類別的物件去。

會不會有點難以理解呢?別擔心,下面我們先用個簡單範例來說明

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 定義父類別
class ClassDemo08:
    # 定義建構子
    def __init__(self, a):
        # 設定父類別的屬性
        self.a = a

    # 定義父類別的方法
    def do_something(self):
        return self.a * self.a

# 定義子類別
class ClassDemo09(ClassDemo08):
    # 定義子類別的方法
    def do_something2(self):
        return self.a * 10

# 以下是執行部分
# 建立 ClassDemo09 型態的變數 d
d = ClassDemo09(7)
# 呼叫父類別的方法
print(d.do_something())
# 呼叫子類別的方法
print(d.do_something2())

# 檔名: class_demo13.py
# 說明:《Python入門指南》的範例程式
# 網站: http://kaiching.org
# 作者: 張凱慶
# 時間: 2023 年 5 月

ClassDemo08 是先定義準備當作父類別,然後繼承的寫法在第 13 行,子類別識別字 ClassDemo09 之後緊接小括弧,小括弧中為父類別的識別字

13
15
16
class ClassDemo09(ClassDemo08):
    def do_something2(self):
        return self.a * 10

子類別 ClassDemo09 中定義子類別的方法 do_something() ,注意這裡子類別都沒有定義跟父類別相同名稱的方法,包括子類別也沒有定義建構子。

繼續看到執行部分,第 20 行建立子類別的物件變數 d

20
22
24
d = ClassDemo09(7)
print(d.do_something())
print(d.do_something2())

建立子類別物件是需要參數 (parameter) 的,這是因為子類別 ClassDemo09 直接套用了父類別 ClassDemo09 的建構子,所以建立子類別變數需要用跟父類別一樣的寫法,也就是需要一個參數,不然會發生少了參數的錯誤。

下面繼續是呼叫父類別的方法,然後呼叫子類別的方法,執行結果如下

$ python class_demo13.py
49
70
$

但是子類別一旦定義跟父類別相同名稱的方法,就涉及改寫 (override) 問題,像是父類別跟子類別都有 do_something() 方法的話,子類別變數呼叫 do_something() 方法是執行子類別物件的的 do_something() 方法,而非父類別物件的 do_something() 方法,如果要讓子類別改寫過的方法使用父類別的內容,就要用內建函數 (function) super() 呼叫,舉例如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 定義父類別
class ClassDemo08:
    # 定義建構子
    def __init__(self, a):
        # 設定父類別的屬性
        self.a = a

    # 定義父類別的方法
    def do_something(self):
        return self.a * self.a

# 定義子類別
class ClassDemo10(ClassDemo08):
    # 改寫父類別的建構子
    def __init__(self, a, b):
        # 呼叫父類別的建構子
        super().__init__(a)
        # 設定子類別的屬性
        self.b = b

    # 改寫父類別的方法
    def do_something(self, selection=None):
        if selection == None:
            return super().do_something()
        else:
            return self.a * self.b

# 以下是執行部分
# 建立父類別的變數 d1
d1 = ClassDemo08(7)
# 呼叫父類別的方法
print(d1.do_something())
# 建立子類別的變數 d2
d2 = ClassDemo10(7, 8)
# 呼叫子類別改寫過的父類別方法
print(d2.do_something())

# 檔名: class_demo14.py
# 說明:《Python入門指南》的範例程式
# 網站: http://kaiching.org
# 作者: 張凱慶
# 時間: 2023 年 5 月

這裡子類別 ClassDemo10 繼承自父類別 ClassDemo08 ,建構子經過改寫,增加屬性 b ,由於父類別有屬性 a ,因此要在子類別的建構子中用 super() 呼叫父類別的建構子,並提供參數 a 給父類別設定屬性 a

15
17
19
    def __init__(self, a, b):
        super().__init__(a)
        self.b = b

子類別 ClassDemo10do_something() 方法也改寫過,這裡增加已經設定預設值的參數 selection ,如果 selection 是預設值 None ,表示要用父類別的 do_something() 方法,如果有提供參數值,那就是使用子類別定義的 do_something()

22
23
24
25
26
    def do_something(self, selection=None):
        if selection == None:
            return super().do_something()
        else:
            return self.a * self.b

底下執行部分分別建立父類別變數 d1 以及子類別變數 d2 ,然後分別印出不帶參數的 do_something() 回傳值

30
32
34
36
d1 = ClassDemo08(7)
print(d1.do_something())
d2 = ClassDemo10(7, 8)
print(d2.do_something())

執行結果如下

$ python class_demo14.py
49
49
$

Python 允許使用多重繼承 (multiple inheritance) ,也就是子類別可以繼承兩個以上的父類別,舉例如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 定義第一個父類別
class ClassDemo08:
    # 定義建構子
    def __init__(self, a):
        # 設定父類別的屬性
        self.a = a

    # 定義第一個父類別的方法
    def do_something(self):
        return self.a * self.a

# 定義第二個父類別
class ClassDemo11:
    # 定義建構子
    def __init__(self, b):
        # 設定父類別的屬性
        self.b = b

    # 定義第二個父類別的方法
    def do_something(self):
        return self.b * 10

# 定義子類別
class ClassDemo12(ClassDemo08, ClassDemo11):
    # 定義建構子
    def __init__(self, a, b):
        # 依次呼叫父類別的建構子
        ClassDemo08.__init__(self, a)
        ClassDemo11.__init__(self, b)

    # 定義使用第二個父類別的方法
    def do_something2(self):
        return ClassDemo11.do_something(self)

# 以下是執行部分
# 建立子類別的變數 d
d = ClassDemo12(7, 8)
# 呼叫第一個父類別的方法
print(d.do_something())
# 呼叫第二個父類別的方法
print(d.do_something2())

# 檔名: class_demo15.py
# 說明:《Python入門指南》的範例程式
# 網站: http://kaiching.org
# 作者: 張凱慶
# 時間: 2023 年 5 月

這裡,子類別 ClassDemo12 繼承 ClassDemo08ClassDemo11 兩個父類別,多重繼承是在子類別識別字後面的小括弧用逗號區隔父類別識別字

24
class ClassDemo12(ClassDemo08, ClassDemo11):

由於兩個父類別各有一個屬性要設定,因此重新定義子類別建構子時要用類別名稱呼叫父類別的建構子

26
28
29
    def __init__(self, a, b):
        ClassDemo08.__init__(self, a)
        ClassDemo11.__init__(self, b)

下面在子類別中定義新的 do_something2() 方法,裡頭呼叫第二個父類別 ClassDemo11do_something() 方法

32
33
    def do_something2(self):
        return ClassDemo11.do_something(self)

這是因為多重繼承中,預設繼承第一個父類別中相同名稱的方法,因此如果要使用第二個父類別中相同名稱的方法,就要額外自行設定呼叫。

super() 的寫法會複雜許多,因此這裡用直接寫類別名稱。

執行部分是建立子類別物件,然後依序印出 do_somehting()do_somehting2() 的回傳值

37
39
41
d = ClassDemo12(7, 8)
print(d.do_something())
print(d.do_something2())

執行結果如下

$ python class_demo15.py
49
80
$

至於多型的概念在 Python 中是無處不在的,這裡舉一個簡單的例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 定義具有相同名稱方法的第一個類別
class Duck:
    def walk(self):
        print("....")

    def swim(self):
        print("))((")

    def sound(self):
        print("呱呱")

# 定義具有相同名稱方法的第二個類別
class Unknown:
    def walk(self):
        print(".*..")

    def swim(self):
        print("(())")

    def sound(self):
        print("哇哇")

# 以下是執行部分
# 在串列中建立兩種型態的物件實體
d = [Duck(), Unknown()]
# 利用 for 迴圈依次呼叫具有相同名稱的方法
for i in d:
    i.walk()
    i.swim()
    i.sound()

# 檔名: class_demo16.py
# 說明:《Python入門指南》的範例程式
# 網站: http://kaiching.org
# 作者: 張凱慶
# 時間: 2023 年 5 月

傳統物件導向程式語言的多型主要是靠繼承關係來達成,其中一個基本概念是透過繼承,讓父類別可以變成子類別的通用型態,倒是這實際討論起來會是本大部頭的書,這裡僅以簡單例子介紹 Python 的多型。

Python 的多型透過類別定義相同的方法來達成,例如這裡第一個 Duck 類別跟第二個 Unknown 類別都有 walk()swim()sound() 三個方法

13
14
15
16
17
18
19
20
21
class Unknown:
    def walk(self):
        print(".*..")

    def swim(self):
        print("(())")

    def sound(self):
        print("哇哇")

這樣的多型概念叫做鴨子型態,中文簡單講就是某個人看見一隻鳥,只要這隻鳥走起路來像鴨子,游泳像鴨子,叫聲也像鴨子,那這個人就會叫那隻鳥為鴨子,換句話說, Python 中只要有一樣的定義都可視作多型。

執行部分是把 Duck()Unknown() 放在 for 迴圈 (loop) 中,依次執行三個相同名稱的方法

25
27
28
29
30
d = [Duck(), Unknown()]
for i in d:
    i.walk()
    i.swim()
    i.sound()

執行結果如下

$ python class_demo16.py
....
))((
呱呱
.*..
(())
哇哇
$

其實 Python 多型最直觀的例子是序列 (sequence) ,串列 (list) 、字串 (string) 、序對 (tuple) 等型態之所以被歸類為序列,不就是因為序列型態有相同的操作方式嗎?

接下來我們繼續看到靜態方法 (static method) 與抽象方法 (abstract method) ,前者是類別可以定義相對於實體方法與類別方法之外的第三種方法,後者則用於繼承。

中英文術語對照
抽象方法abstract method
屬性attribute
類別class
封裝encapsulation
函數function
繼承inheritance
串列list
迴圈loop
成員member
方法method
多重繼承multiple inheritance
物件object
物件導向程式設計object-oriented programming
改寫override
參數parameter
多型polymorphism
序列sequence
靜態方法static method
字串string
子類別subclass
父類別superclass
序對tuple
重點整理
1. 繼承是指類別可以從其他類別擴充屬性跟方法。
2. Python 繼承的寫法是在子類別識別字後面加上小括弧,小括弧內放父類別的識別字。
3. 子類別如果定義跟父類別相同識別字的方法,就是改寫父類別的方法,可利用 super() 呼叫執行父類別方法的內容。
4. Python 中的類別可以繼承超過一個來源,這叫做多重繼承。
5. Python 採用鴨子型態的多型概念。
問題與討論
1. 為什麼要讓類別可以用其他類別擴充屬性跟方法?
2. 多型跟繼承有何不同?
3. 子類別在什麼情況下需要改寫父類別的方法?
4. 多重繼承有什麼優點及缺點?
練習
1. 寫一個程式 exercise1501.py ,裡頭設計一個類別 Point 用作記錄座標,並以參數 xy 設定座標屬性 xy ,另需設定座標類別的字串形式與布林性質。 參考程式碼
2. 承上題,將新程式寫在 exercise1502.py 中,引入上例的 Point ,並設計一個 Animal 類別,用作鬥獸棋棋子的父類別, Animal 類別至少要有名稱 name 、座標 point 、陣營 camp 、生命 health 等屬性,並且有基本的上下左右移動方法。 參考程式碼
3. 承上題,將新程式寫在 exercise1503.py 中,引入 PointAnimal ,並設計繼承 AnimalElephant 類別, Elephant 作為棋子象,裡頭需要定義屬性 food ,設定可以吃的棋子種類。 參考程式碼
4. 承上題,將新程式寫在 exercise1504.py 中,繼續定義 Lion 類別。 參考程式碼
5. 承上題,將新程式寫在 exercise1505.py 中,繼續定義 Tiger 類別。 參考程式碼
6. 承上題,將新程式寫在 exercise1506.py 中,繼續定義 Leopard 類別。 參考程式碼
7. 承上題,將新程式寫在 exercise1507.py 中,繼續定義 Wolf 類別。 參考程式碼
8. 承上題,將新程式寫在 exercise1508.py 中,繼續定義 Dog 類別。 參考程式碼
9. 承上題,將新程式寫在 exercise1509.py 中,繼續定義 Cat 類別。 參考程式碼
10. 承上題,將新程式寫在 exercise1510.py 中,繼續定義 Mouse 類別。 參考程式碼

上一頁 單元 14 - 資料模型、特別屬性與迭代器
回 Python 入門指南 5.0 首頁
下一頁 單元 16 - 靜態方法與抽象方法
回 Python 教材首頁
回程式語言教材首頁