Python 物件導向程式設計(OOP)— 抽象類別(abc 模組)
簡介
在 Python 的物件導向程式設計中,抽象類別提供了一種「規範」的機制:它可以定義子類別必須實作的介面(方法或屬性),卻不提供具體的實作細節。透過抽象類別,我們能夠:
- 避免忘記實作關鍵方法,在開發大型系統時減少錯誤。
- 提升程式的可讀性與可維護性,讓其他開發者一眼就能看出子類別的行為約定。
- 支援多型(polymorphism),讓不同子類別在同一介面下互換使用。
Python 標準庫的 abc(Abstract Base Classes)模組正是為了這個目的而設計的。即使 Python 是動態語言,abc 仍然能在執行時期提供嚴格的檢查,協助我們寫出更可靠的程式碼。
核心概念
1. 為什麼需要抽象類別?
在沒有抽象類別的情況下,我們只能靠 文件說明 或 程式碼註解 來告訴開發者「必須實作 X 方法」。若子類別忘記實作,程式只會在呼叫時拋出 AttributeError,錯誤出現在執行過程的較晚階段,除錯成本較高。使用抽象類別,Python 會在 類別定義階段 就檢查是否實作了所有抽象方法,提前捕捉錯誤。
2. abc 模組的三個核心成員
| 成員 | 功能 | 範例 |
|---|---|---|
ABC |
抽象基底類別,所有抽象類別都應繼承自它。 | class Shape(ABC): |
@abstractmethod |
裝飾器,標記方法為抽象方法,必須在子類別中實作。 | @abstractmethod\ndef area(self): ... |
@abstractproperty(Python 3.3+ 已棄用)或 @property + @abstractmethod |
讓屬性也能成為抽象成員。 | @property\n@abstractmethod\ndef name(self): ... |
註:從 Python 3.3 起,
@abstractproperty被@property搭配@abstractmethod取代,寫法更直觀。
3. 定義抽象類別的基本步驟
from abc import ABC, abstractmethod
class Animal(ABC):
"""所有動物的抽象基底類別"""
@abstractmethod
def speak(self) -> str:
"""子類別必須實作的叫聲方法"""
pass
@property
@abstractmethod
def species(self) -> str:
"""子類別必須提供的物種名稱"""
pass
- 步驟 1:繼承
ABC。 - 步驟 2:使用
@abstractmethod裝飾方法或屬性。 - 步驟 3:在抽象類別中可同時放入一般(非抽象)方法,提供共用的實作。
4. 實作子類別
class Dog(Animal):
"""Dog 必須實作 Animal 定義的抽象成員"""
def speak(self) -> str:
return "汪汪"
@property
def species(self) -> str:
return "Canis lupus familiaris"
如果忘記實作 speak 或 species,Python 會在 類別建立時 拋出 TypeError:
class Cat(Animal):
# 缺少 species 實作
def speak(self) -> str:
return "喵喵"
# TypeError: Can't instantiate abstract class Cat with abstract methods species
5. 多重抽象基底
抽象類別可以同時繼承多個抽象基底,讓子類別必須滿足多個介面:
from abc import ABC, abstractmethod
class Flyer(ABC):
@abstractmethod
def fly(self) -> None:
...
class Swimmer(ABC):
@abstractmethod
def swim(self) -> None:
...
class Duck(Flyer, Swimmer):
def fly(self):
print("Duck flies")
def swim(self):
print("Duck swims")
6. 抽象類別也可以有「類別方法」抽象
有時候我們希望子類別必須提供某個 類別層級 的工廠方法:
class Shape(ABC):
@classmethod
@abstractmethod
def from_json(cls, data: dict):
"""根據 JSON 資料建立對應的 Shape 物件"""
pass
子類別必須實作 from_json,且必須是 @classmethod,否則仍會被視為抽象。
7. 使用 register 進行虛擬子類別(duck typing)
abc.ABCMeta.register 允許我們把一個普通類別「註冊」為抽象基底的子類別,而不必繼承它。這在需要兼容第三方類別時非常有用:
class Iterable(ABC):
@abstractmethod
def __iter__(self):
...
class MyList:
def __iter__(self):
return iter([1, 2, 3])
Iterable.register(MyList) # 現在 isinstance(MyList(), Iterable) 為 True
程式碼範例
以下提供 五個實務範例,從最基礎到較進階的使用情境,幫助讀者快速上手。
範例 1:最簡單的抽象類別 – 圖形基底
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
"""回傳圖形面積"""
@abstractmethod
def perimeter(self) -> float:
"""回傳圖形周長"""
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
import math
return math.pi * self.radius ** 2
def perimeter(self) -> float:
import math
return 2 * math.pi * self.radius
說明:
Shape定義了兩個必須實作的抽象方法,Circle完整提供實作,讓我們可以直接呼叫area()、perimeter()。
範例 2:抽象屬性 + 一般方法
from abc import ABC, abstractmethod
class Employee(ABC):
@property
@abstractmethod
def salary(self) -> float:
"""回傳月薪"""
def annual_bonus(self) -> float:
"""預設的年終獎金計算方式,子類別可自行覆寫"""
return self.salary * 0.1
class Engineer(Employee):
def __init__(self, salary: float):
self._salary = salary
@property
def salary(self) -> float:
return self._salary
說明:
Employee抽象屬性salary必須在子類別中實作;而annual_bonus為一般方法,提供共用的計算邏輯。
範例 3:多重抽象基底 – 飛行與游泳
from abc import ABC, abstractmethod
class Flyer(ABC):
@abstractmethod
def fly(self) -> None:
...
class Swimmer(ABC):
@abstractmethod
def swim(self) -> None:
...
class Penguin(Flyer, Swimmer):
def fly(self):
print("Penguin can't really fly, but it can glide.")
def swim(self):
print("Penguin swims elegantly.")
說明:
Penguin同時滿足Flyer與Swimmer的抽象要求,展示了 多重繼承 在 OOP 中的實用性。
範例 4:抽象類別作為工廠(類別方法抽象)
from abc import ABC, abstractmethod
import json
class Notification(ABC):
@abstractmethod
def send(self, message: str) -> None:
...
@classmethod
@abstractmethod
def from_config(cls, cfg: dict):
"""根據設定字典產生具體的 Notification 實例"""
...
class EmailNotification(Notification):
def __init__(self, smtp_server: str):
self.smtp_server = smtp_server
def send(self, message: str) -> None:
print(f"Sending email via {self.smtp_server}: {message}")
@classmethod
def from_config(cls, cfg: dict):
return cls(smtp_server=cfg["smtp_server"])
# 使用範例
config = {"smtp_server": "smtp.example.com"}
notifier = EmailNotification.from_config(config)
notifier.send("Hello World!")
說明:
Notification透過抽象類別方法規範了「工廠」的建立方式,讓不同的通知渠道(Email、SMS、Push)都能遵循相同的介面。
範例 5:虛擬子類別(register) – 與第三方類別合作
假設我們使用第三方的 numpy.ndarray,但想把它視為我們自訂的 Vector 抽象基底:
from abc import ABC, abstractmethod
import numpy as np
class Vector(ABC):
@abstractmethod
def norm(self) -> float:
"""計算向量的 L2 範數"""
# 將 numpy.ndarray 註冊為 Vector 的虛擬子類別
Vector.register(np.ndarray)
# 為了讓 ndarray 具備 norm 方法,我們利用 monkey patch
def ndarray_norm(self):
return float(np.linalg.norm(self))
np.ndarray.norm = ndarray_norm
v = np.array([3, 4])
print(isinstance(v, Vector)) # True
print(v.norm()) # 5.0
說明:不必改變第三方類別的繼承樹,仍能透過
isinstance判斷其是否符合抽象介面,並以 duck typing 的方式擴充功能。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決方案 / 最佳實踐 |
|---|---|---|
| 忘記在子類別實作抽象方法 | TypeError 在實例化時拋出,若未留意會導致程式直接崩潰。 |
使用 IDE / 靜態分析工具(如 mypy)提前偵測未實作的方法。 |
| 抽象屬性寫成普通屬性 | 子類別雖然能被實例化,但介面不一致,導致其他程式碼錯誤。 | 使用 @property + @abstractmethod 明確宣告抽象屬性。 |
| 在抽象類別中放入過多實作 | 抽象類別變成「混合類別」,會降低可讀性。 | 保持抽象類別僅定義介面,共用邏輯可抽成 Mixin 或 Helper。 |
| 多重繼承時產生菱形繼承問題 | super() 呼叫順序不如預期,可能重複執行建構子。 |
使用 super() 並遵循 MRO(Method Resolution Order),或改用 組合(composition)。 |
使用 register 時忘記實作抽象方法 |
虛擬子類別仍被視為抽象,isinstance 會回傳 True,但實際呼叫抽象方法會失敗。 |
在註冊前確保第三方類別已具備所需方法(可透過 monkey patch 或 wrapper)。 |
最佳實踐總結:
- 明確分離抽象介面與實作:抽象類別只負責規範,實作交給子類別或 Mixin。
- 在抽象方法上加入完整的 docstring,讓使用者一眼就能知道預期的參數與回傳值。
- 配合型別提示(type hint),提升 IDE 自動完成與靜態檢查的效果。
- 盡量使用
@classmethod抽象方法 來建立「工廠」或「序列化」的統一入口。 - 測試抽象類別的子類別:寫測試時先建立抽象基底的「測試雙」(test double),確保所有抽象方法都被正確實作。
實際應用場景
插件系統(Plugin Architecture)
- 核心程式只依賴抽象基底(如
PluginInterface),每個插件實作自己的子類別。透過abc,核心在載入插件前就能確保插件提供必要的initialize、execute、shutdown方法。
- 核心程式只依賴抽象基底(如
資料庫存取層(Repository Pattern)
- 定義
AbstractRepository抽象類別,規範add、get_by_id、list_all等 CRUD 方法。不同資料庫(MySQL、MongoDB、SQLite)各自實作子類別,讓服務層只與抽象介面互動。
- 定義
網路協定實作
- 抽象類別
Transport定義connect、send、receive。TCP、UDP、WebSocket 等各自繼承Transport,提供不同的傳輸細節,同時保證介面一致。
- 抽象類別
機器學習模型封裝
ModelBase抽象類別提供fit、predict、save、load。TensorFlow、PyTorch、scikit-learn 的模型都實作子類別,使得上層的訓練管線可以無縫切換模型框架。
國際化(i18n)與本地化(l10n)
- 抽象類別
Translator定義translate(key: str) -> str。不同語言的翻譯檔(JSON、YAML、資料庫)各自實作Translator,讓 UI 只需要呼叫同一個介面。
- 抽象類別
總結
- 抽象類別(
abc模組) 為 Python 的物件導向提供了「介面」的概念,使得程式在設計階段即可捕捉未實作的方法或屬性。 - 透過
ABC、@abstractmethod、@property+@abstractmethod等工具,我們可以清晰地定義 必須實作的合約,同時保留共用的實作邏輯。 - 多重抽象基底、類別方法抽象、以及
register虛擬子類別的技巧,讓抽象類別在 插件、工廠、跨框架整合 等實務情境中發揮關鍵作用。 - 避免常見陷阱(忘記實作、屬性寫錯、菱形繼承等)並遵循最佳實踐(清晰 docstring、型別提示、測試),即可在大型專案中安全、有效地運用抽象類別。
掌握了 abc 模組的使用方式後,你的 Python 程式碼將更具可讀性、可維護性與擴充性,為未來的系統演進奠定堅實基礎。祝你寫程式愉快,持續探索 OOP 的無限可能!