本文 AI 產出,尚未審核

Python 物件導向程式設計(OOP)— 抽象類別(abc 模組)

簡介

在 Python 的物件導向程式設計中,抽象類別提供了一種「規範」的機制:它可以定義子類別必須實作的介面(方法或屬性),卻不提供具體的實作細節。透過抽象類別,我們能夠:

  1. 避免忘記實作關鍵方法,在開發大型系統時減少錯誤。
  2. 提升程式的可讀性與可維護性,讓其他開發者一眼就能看出子類別的行為約定。
  3. 支援多型(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"

如果忘記實作 speakspecies,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 同時滿足 FlyerSwimmer 的抽象要求,展示了 多重繼承 在 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 明確宣告抽象屬性。
在抽象類別中放入過多實作 抽象類別變成「混合類別」,會降低可讀性。 保持抽象類別僅定義介面,共用邏輯可抽成 MixinHelper
多重繼承時產生菱形繼承問題 super() 呼叫順序不如預期,可能重複執行建構子。 使用 super() 並遵循 MRO(Method Resolution Order),或改用 組合(composition)
使用 register 時忘記實作抽象方法 虛擬子類別仍被視為抽象,isinstance 會回傳 True,但實際呼叫抽象方法會失敗。 在註冊前確保第三方類別已具備所需方法(可透過 monkey patch 或 wrapper)。

最佳實踐總結

  1. 明確分離抽象介面與實作:抽象類別只負責規範,實作交給子類別或 Mixin。
  2. 在抽象方法上加入完整的 docstring,讓使用者一眼就能知道預期的參數與回傳值。
  3. 配合型別提示(type hint),提升 IDE 自動完成與靜態檢查的效果。
  4. 盡量使用 @classmethod 抽象方法 來建立「工廠」或「序列化」的統一入口。
  5. 測試抽象類別的子類別:寫測試時先建立抽象基底的「測試雙」(test double),確保所有抽象方法都被正確實作。

實際應用場景

  1. 插件系統(Plugin Architecture)

    • 核心程式只依賴抽象基底(如 PluginInterface),每個插件實作自己的子類別。透過 abc,核心在載入插件前就能確保插件提供必要的 initializeexecuteshutdown 方法。
  2. 資料庫存取層(Repository Pattern)

    • 定義 AbstractRepository 抽象類別,規範 addget_by_idlist_all 等 CRUD 方法。不同資料庫(MySQL、MongoDB、SQLite)各自實作子類別,讓服務層只與抽象介面互動。
  3. 網路協定實作

    • 抽象類別 Transport 定義 connectsendreceive。TCP、UDP、WebSocket 等各自繼承 Transport,提供不同的傳輸細節,同時保證介面一致。
  4. 機器學習模型封裝

    • ModelBase 抽象類別提供 fitpredictsaveload。TensorFlow、PyTorch、scikit-learn 的模型都實作子類別,使得上層的訓練管線可以無縫切換模型框架。
  5. 國際化(i18n)與本地化(l10n)

    • 抽象類別 Translator 定義 translate(key: str) -> str。不同語言的翻譯檔(JSON、YAML、資料庫)各自實作 Translator,讓 UI 只需要呼叫同一個介面。

總結

  • 抽象類別(abc 模組) 為 Python 的物件導向提供了「介面」的概念,使得程式在設計階段即可捕捉未實作的方法或屬性。
  • 透過 ABC@abstractmethod@property + @abstractmethod 等工具,我們可以清晰地定義 必須實作的合約,同時保留共用的實作邏輯。
  • 多重抽象基底、類別方法抽象、以及 register 虛擬子類別的技巧,讓抽象類別在 插件、工廠、跨框架整合 等實務情境中發揮關鍵作用。
  • 避免常見陷阱(忘記實作、屬性寫錯、菱形繼承等)並遵循最佳實踐(清晰 docstring、型別提示、測試),即可在大型專案中安全、有效地運用抽象類別。

掌握了 abc 模組的使用方式後,你的 Python 程式碼將更具可讀性、可維護性與擴充性,為未來的系統演進奠定堅實基礎。祝你寫程式愉快,持續探索 OOP 的無限可能!