Python 課程 – 物件導向程式設計(OOP)
主題:方法覆寫(Method Overriding)
簡介
在物件導向程式設計(OOP)裡,類別(class) 讓我們可以把資料與行為封裝在一起,而繼承(inheritance) 則提供了「類別之間的關係」與「程式碼重用」的機制。當子類別(sub‑class)繼承父類別(super‑class)的屬性與方法後,常會遇到「同名方法需要不同實作」的情況,這時就會用到 方法覆寫(method overriding)。
方法覆寫不僅是語法層面的技巧,更是設計上「多型(polymorphism)」的核心概念。透過覆寫,我們可以讓子類別在呼叫同一個方法時,呈現出符合自身需求的行為,從而提升程式的彈性、可讀性與可維護性。
核心概念
1. 什麼是方法覆寫?
在 Python 中,子類別可以 重新定義(override)父類別已經存在的同名方法。當我們對子類別的實例呼叫這個方法時,Python 會先在子類別的命名空間尋找,若找到就直接執行子類別的版本;若找不到,才會往上尋找父類別的實作。
重點:方法覆寫不會改變父類別本身,只是「在子類別裡提供另一套實作」。
2. 使用 super() 呼叫父類別的方法
有時候我們想在子類別的覆寫方法中,先保留父類別原有的行為,再加入額外的處理。這時可以使用 super() 取得父類別的物件,呼叫其原本的方法。
class Animal:
def speak(self):
print("Animal makes a sound")
class Dog(Animal):
def speak(self):
# 先執行父類別的行為
super().speak()
# 再加入子類別的行為
print("Dog barks")
3. 覆寫與抽象方法(abstract method)
在使用抽象基底類別(abc.ABC)時,父類別會定義 抽象方法,強制子類別必須實作(override)這些方法,否則無法實例化。
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass # 必須在子類別覆寫
4. 多層繼承的覆寫順序(MRO)
若一個類別同時繼承多個父類別,Python 會根據 Method Resolution Order (MRO) 依序搜尋方法。super() 會依照 MRO 呼叫下一個類別的相同方法,這對於 鑽石繼承(diamond inheritance)尤為重要。
class A:
def hello(self):
print("Hello from A")
class B(A):
def hello(self):
print("Hello from B")
super().hello()
class C(A):
def hello(self):
print("Hello from C")
super().hello()
class D(B, C):
pass
d = D()
d.hello() # 輸出順序遵循 D → B → C → A
程式碼範例
以下提供 5 個實用範例,展示方法覆寫在不同情境下的寫法與意義。
範例 1:最基本的覆寫
class Vehicle:
def move(self):
print("Vehicle is moving")
class Car(Vehicle):
def move(self):
print("Car drives on the road")
v = Vehicle()
c = Car()
v.move() # Vehicle is moving
c.move() # Car drives on the road
說明:
Car重新定義了move,即使Vehicle仍保有原本的實作。
範例 2:使用 super() 加強父類別行為
class Logger:
def log(self, msg):
print(f"[LOG] {msg}")
class FileLogger(Logger):
def log(self, msg):
super().log(msg) # 先印出一般 LOG
with open("app.log", "a") as f:
f.write(msg + "\n") # 再寫入檔案
logger = FileLogger()
logger.log("程式啟動")
# 螢幕上會顯示 [LOG] 程式啟動,且同時寫入 app.log
範例 3:抽象基底類別的必須覆寫
from abc import ABC, abstractmethod
class PaymentProcessor(ABC):
@abstractmethod
def pay(self, amount):
"""執行付款動作"""
class CreditCardProcessor(PaymentProcessor):
def pay(self, amount):
print(f"使用信用卡付款 {amount} 元")
processor = CreditCardProcessor()
processor.pay(1200) # 使用信用卡付款 1200 元
重點:若子類別忘記實作
pay,Python 會在建立實例時拋出TypeError。
範例 4:多層繼承與 MRO
class Base:
def greet(self):
print("Hello from Base")
class MixinA(Base):
def greet(self):
print("Hello from MixinA")
super().greet()
class MixinB(Base):
def greet(self):
print("Hello from MixinB")
super().greet()
class Concrete(MixinA, MixinB):
pass
obj = Concrete()
obj.greet()
# 輸出:
# Hello from MixinA
# Hello from MixinB
# Hello from Base
說明:
Concrete依照 MRO (Concrete → MixinA → MixinB → Base) 呼叫greet。
範例 5:覆寫 __str__ 讓物件印出易讀資訊
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
# 預設會印出 <Person object at 0x...>
# 我們覆寫 __str__ 讓它更友善
def __str__(self):
return f"Person(name={self.name}, age={self.age})"
p = Person("小明", 28)
print(p) # Person(name=小明, age=28)
技巧:覆寫特殊方法(如
__str__、__repr__)可提升除錯與日誌的可讀性。
常見陷阱與最佳實踐
| 陷阱 | 可能的結果 | 解決方式 / 最佳實踐 |
|---|---|---|
忘記呼叫 super() |
子類別覆寫後,父類別的重要初始化或清理程式碼不會執行,導致資源洩漏或狀態不完整。 | 若需要保留父類別行為,務必在覆寫方法中加入 super().method_name(...)。 |
| 覆寫時改變參數簽名 | 呼叫方仍以父類別的簽名傳參,會拋出 TypeError 或產生不預期的行為。 |
保持相同的 參數數量與名稱,或使用 *args, **kwargs 轉發。 |
| 多層繼承時忘記 MRO | 子類別的 super() 可能呼叫錯誤的父類別,導致重複執行或根本不執行。 |
熟悉 Method Resolution Order,可使用 ClassName.__mro__ 觀察繼承順序。 |
| 抽象方法未實作 | 直接實例化子類別會得到 TypeError: Can't instantiate abstract class …。 |
確認所有 @abstractmethod 都已在子類別完成覆寫。 |
| 過度覆寫 | 子類別的行為與父類別差異太大,失去「is‑a」關係的語意,程式碼難以理解。 | 遵循 Liskov 替換原則(LSP):子類別應該可以在任何需要父類別的地方安全替換使用。 |
最佳實踐小結:
- 保持簽名一致:除非有特別需求,覆寫方法的參數清單應與父類別相同。
- 使用
super():盡可能呼叫父類別的實作,尤其是__init__、__enter__/__exit__等資源管理相關的方法。 - 文件說明:在覆寫的方法上加上 docstring,說明與父類別的差異或新增的行為。
- 測試覆寫行為:撰寫單元測試,確保子類別在覆寫後仍符合預期功能。
- 遵守 LSP:子類別的行為不應違背父類別的契約,避免「違反預期」的例外。
實際應用場景
| 場景 | 為什麼需要覆寫 | 範例說明 |
|---|---|---|
| Web 框架的視圖(View) | 基底視圖提供通用的請求處理流程,子類別根據不同路由實作具體回應。 | Django 的 View 類別,子類別覆寫 get()、post() 等方法。 |
| 資料庫模型的儲存行為 | 基本模型只負責欄位定義,子類別在儲存前需要自訂驗證或自動填充欄位。 | Django Model.save() 覆寫,加入 slugify 或 updated_at。 |
| 遊戲角色的行為 | 父類別 Character 定義共通屬性,子類別(如 Wizard、Warrior)覆寫 attack() 產生不同攻擊方式。 |
Wizard.attack() 施放魔法;Warrior.attack() 揮舞武器。 |
| 訊息日誌系統 | 基礎日誌只寫入標準輸出,子類別可覆寫寫入檔案、資料庫或遠端服務。 | 前文的 FileLogger 範例。 |
| 插件機制 | 主程式提供抽象介面,外部插件必須覆寫介面方法才能被正確呼叫。 | PluginBase 抽象類別,第三方套件實作 execute()。 |
總結
方法覆寫是 Python 物件導向 中不可或缺的技巧,讓我們能在保留父類別共通行為的同時,為子類別注入專屬的邏輯。掌握以下要點,就能在實務開發中有效運用:
- 保持方法簽名一致,避免不必要的錯誤。
- 適時使用
super(),確保父類別的初始化或清理工作不會遺漏。 - 了解 MRO,在多重繼承環境下正確呼叫父類別方法。
- 遵守 Liskov 替換原則,讓子類別仍能在父類別預期的情境中安全使用。
- 撰寫文件與測試,讓覆寫的行為清晰可追蹤。
透過上述概念與實例,你已具備在日常 Python 專案中 自信地運用方法覆寫 的能力。未來不論是建構框架、設計插件系統,或是開發大型業務邏輯,都能從這個基礎出發,寫出更具彈性、可維護的程式碼。祝你在 OOP 的旅程中持續成長,寫出更優雅的 Python!