Python 入門指南 5.0
單元 15 - 繼承與多型
物件導向程式設計 (object-oriented programming) 有三大基本特性,分別是封裝 (encapsulation) 、繼承 (inheritance) 及多型 (polymorphism)
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
|
子類別 ClassDemo10 的 do_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 繼承 ClassDemo08 與 ClassDemo11 兩個父類別,多重繼承是在子類別識別字後面的小括弧用逗號區隔父類別識別字
24 | class ClassDemo12(ClassDemo08, ClassDemo11):
|
由於兩個父類別各有一個屬性要設定,因此重新定義子類別建構子時要用類別名稱呼叫父類別的建構子
26 28 29 | def __init__(self, a, b):
ClassDemo08.__init__(self, a)
ClassDemo11.__init__(self, b)
|
下面在子類別中定義新的 do_something2() 方法,裡頭呼叫第二個父類別 ClassDemo11 的 do_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 用作記錄座標,並以參數 x 及 y 設定座標屬性 x 與 y ,另需設定座標類別的字串形式與布林性質。 參考程式碼 |
2. 承上題,將新程式寫在 exercise1502.py 中,引入上例的 Point ,並設計一個 Animal 類別,用作鬥獸棋棋子的父類別, Animal 類別至少要有名稱 name 、座標 point 、陣營 camp 、生命 health 等屬性,並且有基本的上下左右移動方法。 參考程式碼 |
3. 承上題,將新程式寫在 exercise1503.py 中,引入 Point 及 Animal ,並設計繼承 Animal 的 Elephant 類別, 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 類別。 參考程式碼 |