Python :: 共同行為與is a
文章推薦指數: 80 %
由於Python 是動態定型語言,若想透過變數操作物件的某個方法,只要確認該 ... 從方才的範例也可以看到,如果父類別定義了 __init__ ,子類別沒有定義 ...
OPENHOME.CC
Python
|起步走
Hello,Python
簡介模組
IO/格式/編碼
|內建型態
數值
字串
清單
集合
字典
tuple
|基本運算
變數
算術運算
比較、指定、邏輯運算
位元運算
|流程語法
if分支判斷
while迴圈、for迭代
forComprehension
初試match/case
|函式入門
定義函式
一級函式、lambda運算式
初探變數範圍
yield產生器
|封裝
類別入門
屬性與方法
屬性名稱空間
特殊方法
callable物件
|繼承
共同行為與isa
使用enum列舉
多重繼承與Mixin
|例外處理
使用try、except
例外繼承架構
raise例外
使用else、finally
使用withas
使用assert
|模組/套件
管理模組名稱
模組路徑
使用套件
|metaprogramming
__slots__、__abstractmethods__、__init_subclass__
__getattribute__、__getattr__、__setattr__、__delattr__
裝飾器
描述器
type類別
metaclass
super與mro
GitHub
Twitter
Facebook
LinkedIn
2DDesigns
3DDesigns
Tags
BuiltwithbyHugo
HOME>
Python>
繼承>
共同行為與isa
繼承共同行為
多型/鴨子定型
重新定義方法
抽象類別/方法
inheritance
objectoriented
ducktyping
polymorphism
subtypepolymorphism
Liskovsubstitution
super
typesystem
refactor
共同行為與isa
April28,2022
物件導向中,子類別繼承(inherit)父類別,可避免重複的行為與實作定義,不過並非為了避免重複定義行為與實作就得使用繼承,濫用繼承而導致程式維護上的問題時有所聞,如何正確判斷使用繼承的時機,以及繼承之後如何活用多型(polymorphism),才是學習繼承時的重點。
繼承共同行為
繼承基本上是為了避免多個類別間重複實作相同行為。
以實際的例子來說明比較清楚,假設你在正開發一款RPG(Role-playinggame)遊戲,一開始設定的角色有劍士與魔法師。
首先你定義了劍士類別:
classSwordsMan:
def__init__(self,name,level,blood):
self.name=name#角色名稱
self.level=level#角色等級
self.blood=blood#角色血量
deffight(self):
print('揮劍攻擊')
def__str__(self):
return"('{name}',{level},{blood})".format(**vars(self))
def__repr__(self):
returnself.__str__()
劍士擁有名稱、等級與血量等屬性,可以揮劍攻擊,為了方便顯示劍士的屬性,定義了__str__方法,並讓__repr__的字串描述直接傳回了__str__的結果。
接著你為魔法師定義類別:
classMagician:
def__init__(self,name,level,blood):
self.name=name#角色名稱
self.level=level#角色等級
self.blood=blood#角色血量
deffight(self):
print('魔法攻擊')
defcure(self):
print('魔法治療')
def__str__(self):
return"('{name}',{level},{blood})".format(**vars(self))
def__repr__(self):
returnself.__str__()
你注意到什麼呢?只要是遊戲中的角色,都會具有角色名稱、等級與血量,也定義了相同的__str__與__repr__方法,Magician與SwordsMan中相對應的程式碼重複了。
重複在程式設計上,就是不好的訊號。
舉個例子來說,如果要將name、level、blood更改為其他名稱,就要修改SwordsMan與Magician兩個類別,如果有更多類別具有重複的程式碼,就得修改更多類別,造成維護上的不便。
可以思考一下,會造成這樣的不便,Magician與SwordsMan是否具有isa的關係?共用了相同的屬性?劍士與魔法師是否都是一種角色?如果是的話,可以把相同的程式碼提昇(Pullup)至父類別Role,並讓SwordsMan與Magician類別都繼承自Role類別:
classRole:
def__init__(self,name,level,blood):
self.name=name#角色名稱
self.level=level#角色等級
self.blood=blood#角色血量
def__str__(self):
return"('{name}',{level},{blood})".format(**vars(self))
def__repr__(self):
returnself.__str__()
classSwordsMan(Role):
deffight(self):
print('揮劍攻擊')
classMagician(Role):
deffight(self):
print('魔法攻擊')
defcure(self):
print('魔法治療')
Role類別沒什麼特別之處,不過是將先前的SwordsMan與Magician重複的程式碼,都定義在Role類別之中。
接著SwordsMan類別在定義時,類別名稱旁邊多了個括號,並指定了Role,這在Python代表著,SwordsMan繼承了Role已定義的程式碼,兩者具有isa的關係,SwordsMan是一種Role,接著SwordsMan定義了自己的fight方法。
類似地,Magician也繼承了Role類別,並且定義了自己的fight與cure方法。
如何看出確實有繼承了呢?以下簡單的程式可以看出:
swordsman=SwordsMan('Justin',1,200)
print('SwordsMan',swordsman)
magician=Magician('Monica',1,100)
print('Magician',magician)
在執行print('劍士',swordsman)與print('魔法師',magician)時,會呼叫swordsman與magician的__str__方法,雖然在SwordsMan與Magician類別的定義中,沒有看到定義了__str__方法,但是它們都從Role繼承下來了,執行的結果如下:
SwordsMan('Justin',1,200)
Magician('Monica',1,100)
可以看到,__str__傳回的字串描述,確實就是Role類別中定義的結果。
繼承的好處之一,就是若要將name、level、blood改為其他名稱,只需修改Role類別的程式碼,繼承Role的子類別無需修改。
多型/鴨子定型
現在有個需求,請設計一個函式,可以播放角色屬性與攻擊動畫,這可以如下撰寫程式:
defdraw_fight(role):
print(role,end='')
role.fight()
swordsman=SwordsMan('Justin',1,200)
draw_fight(swordsman)
magician=Magician('Monica',1,100)
draw_fight(magician)
這邊的draw_fight函式中,直接呼叫了role的fight方法,如果是fight(swordsman),那麼role就是參考了swordsman參考的實例,這時role.fight()就相當於swordsman.fight(),同樣地,如果是fight(magician),role.fight()就相當於magician.fight()了。
執行結果如下:
('Justin',1,200)揮劍攻擊
('Monica',1,100)魔法攻擊
從繼承的角度來看,這種行為稱為多型,或者進一步地說,是次型態多型(subtypepolymorphism)的一種形式,就draw_fight的角度來看,不管是換成SwordsMan或Magician的實例,draw_fight的行為都是放角色屬性與攻擊動畫,符合里氏替換(Liskovsubstitution)原則。
由於Python是動態定型語言,若想透過變數操作物件的某個方法,只要確認該物件確實有該方法即可,從這個角度來看,這就是鴨子定型罷了,也就是說,只要物件上擁有fight方法就可以傳入draw_fight函式。
例如:
>>>classDuck:
...pass
...
>>>duck=Duck()
>>>duck.fight=lambda:print('呱呱')
>>>draw_fight(duck)
<__main__.duckobjectat0x00000211e00c92e0>呱呱
>>>
可以看到,在這邊隨便定義了Duck類別,建立一個實例,臨時指定一個lambda函式給fight屬性,仍然可以傳給draw_fight函式執行,因為Duck並沒有定義__str__,因此使用的是預設的__str__實作,因而看到了<__main__.duckobjectat0x00000211e00c92e0>的結果。
重新定義方法
方才的draw_fight函式若傳入SwordsMan或Magician實例時,各自會顯示('Justin',1,200)揮劍攻擊或('Monica',1,100)魔法攻擊,如果想顯示SwordsMan('Justin',1,200)揮劍攻擊或Magician('Monica',1,100)魔法攻擊的話,要怎麼做呢?
你也許會想判斷傳入的物件,到底是SwordsMan或Magician的實例,然後分別顯示劍士或魔法師的字樣,在Python中,確實有個isinstance函式,可以進行這類的判斷。
例如:
defdraw_fight(role):
ifisinstance(role,rpg.SwordsMan):
print('SwordsMan',end='')
elifisinstance(role,rpg.Magician):
print('Magician',end='')
print(role,end='')
role.fight()
isinstance函式可用來進行執行時期型態檢查,不過每當想要isinstance函式時,要再多想一下,有沒有其他的設計方式。
以這邊的例子來說,若是未來有更多角色的話,勢必要增加更多型態檢查的判斷式,在多數的情況下,檢查型態而給予不同的流程行為,對於程式的維護性有著不良的影響,應該避免。
確實在某些特定的情況下,還是免不了要判斷物件的種類,並給予不同的流程,不過多數情況下,應優先選擇思考物件的行為。
那麼該怎麼做呢?print(role,end='')時,既然實際上是取得role參考實例的__str__傳回的字串並顯示,目前__str__的行為是定義在Role類別而繼承下來,那麼可否分別重新定義SwordsMan與Magician的__str__行為,讓它們各自能增加劍士或魔法師的字樣如何?
可以這麼做,不過,並不用單純地在SwordsMan或Magician定義以下的__str__:
略…
def__str__(self):
return"SwordsMan('{name}',{level},{blood})".format(**vars(self))
略…
def__str__(self):
return"Magician('{name}',{level},{blood})".format(**vars(self))
因為實際上,Role的__str__傳回的字串,只要各自在前面附加上劍士或魔法師就可以了,在繼承後若打算基於父類別的方法實作,來重新定義某個方法,可以使用super來呼叫父類別方法。
例如:
classRole:
略…
def__str__(self):
return'({name},{level},{blood})'.format(**vars(self))
def__repr__(self):
returnself.__str__()
classSwordsMan(Role):
deffight(self):
print('揮劍攻擊')
def__str__(self):
returnf'SwordsMan{super().__str__()}'
classMagician(Role):
deffight(self):
print('魔法攻擊')
defcure(self):
print('魔法治療')
def__str__(self):
returnf'Magician{super().__str__()}'
在重新定義SwordsMan的__str__方法時,呼叫了super().__str__(),這會執行父類別Role中定義的__str__方法並傳回字串,這個字串與'SwordsMan'串接,就會是想要的結果,同樣地,在重新定義Magician的__str__方法時,也是使用super().__str__()取得結果,然後串接'Magician'字串。
從方才的範例也可以看到,如果父類別定義了__init__,子類別沒有定義__init__,那麼建立子類別實例時,指定的引數會自動呼叫父類別的__init__;如果子類別定義了__init__,就得明確地決定要不要呼叫父類別的__init__方法,預設是不會呼叫。
可以使用super來呼叫父類別定義的__init__方法,例如,若SwordsMan定義了__init__,可以如下呼叫Role的__init__方法:
classSwordsMan(Role):
def__init__(self,name,level,blood):
super().__init__(name,level,blood)
...其他程式碼
抽象類別/方法
有時候,希望提醒或說是強制,子類別一定要實作某個方法,也許是怕其他開發者在實作時打錯了方法名稱,像是fight打成了figth,也許是有太多行為必須實作,怕不小心遺漏了其中一兩個。
如果希望子類別在繼承之後,一定要實作的方法,可以繼承abc模組的ABC類別,並在指定的方法上標註abc模組的@abstractmethod來達到需求。
例如,若想強制Role的子類別一定要實作fight方法,可以如下:
fromabcimportABC,abstractmethod
classRole(ABC):
def__init__(self,name,level,blood):
self.name=name#角色名稱
self.level=level#角色等級
self.blood=blood#角色血量
@abstractmethod
deffight(self):
...
def__str__(self):
return"('{name}',{level},{blood})".format(**vars(self))
def__repr__(self):
returnself.__str__()
略…
接著,在fight方法上標註了@abstractmethod,由於Role只是個通用的父類別,並不知道具體的各個角色會如何進行攻擊,也就不用有相關的程式碼實作,因此直接在fight方法的本體中使用...,表示本體省略(也可以使用pass),必要時也可以使用raiseNotImplementedError,這代表著拋出錯誤,除了在程式碼上清楚地表示這是個未實作的方法,也表示子類別不能透過super來呼叫這個方法。
在Python中,abc或ABC這個字樣,是指AbstractBaseClass,也就是抽象基礎類別,通常這些類別已實作了一些基礎行為,開發者可根據需求,使用不同的ABC來實作出想要的功能,不用一切從無到有親手打造。
如上定義了Role類別,就不能使用Role來建構物件了,否則執行時期會發生TypeError,如果有個類別繼承了Role類別,沒有實作fight方法,執行時期在實例化時也會發生TypeError。
然而,先前的SwordsMan與Magician,由於已經實作了fight方法,因此可以順利地拿來建構物件。
Python3.8以後,如果想限定某類別在繼承體系中是最後一個,不能再有子類別,也就是不能被繼承,可以透過typing模組的final裝飾器,定義方法時也可以使用final裝飾器,表示最後一次定義該方法,子類別不可以重新定義該方法。
延伸文章資訊
- 1[Python 學習筆記] 函式、類別與模組: 類別(抽象類別) - 程式扎記
像Python 這類的動態語言,沒有Java 的abstract 或interface 這種機制來規範一個類別所需實作的介面,遵循物件之間的協定基本上是開發人員的自我 ...
- 2【Python 教學】OOP 繼承/封裝/多型基本用法Example - 測試先生
Python 繼承基本用法(inheritance) example. 如何使用繼承(inheritance)的寫法呢? DisplayNameAge為父類別(Base class),Enter...
- 3物件導向武功秘笈(2):招式篇— Python與Java的 ... - YC Note
抽象化:抽象類別(Abstract Class)、抽象方法(Abstract Method)和接口(Interface). 事實上,剛剛使用 Animal 的方法並不是很正確,我們將 Anima...
- 4[Python物件導向]Python多型(Polymorphism)實用教學
必須透過繼承(Inheritance)的類別來進行抽象方法(Abstract Method)的實作,如下範例:. from abc import ABC, abstractmethod ...
- 5[Day 13] 談談抽象這件事 - iT 邦幫忙
而先前我們一直提到interface 也是在定義一些沒有實作的方法,那麼 ... 在Python 並沒有像Java、Scala 直接有abstract class 這樣的關鍵字來宣告抽象類別,而...