Python :: 共同行為與is a

文章推薦指數: 80 %
投票人數:10人

由於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裝飾器,表示最後一次定義該方法,子類別不可以重新定義該方法。



請為這篇文章評分?