Python 物件導向程式設計(OOP)
單元:繼承(單繼承、多繼承)
簡介
在物件導向程式設計(OOP)中,繼承是最核心的概念之一。透過繼承,我們可以讓一個類別(子類別)直接取得另一個類別(父類別)已實作好的屬性與方法,從而減少重複程式碼、提升程式的可維護性與可擴充性。
在實務開發裡,無論是設計 GUI 元件、建立資料模型,或是開發框架與套件,都會頻繁運用單繼承與多繼承的技巧。掌握正確的繼承寫法,不僅能讓程式結構更清晰,也能避免因繼承層級過深或衝突而產生的難以除錯的問題。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐步帶領讀者深入了解 Python 中的繼承機制,並提供可直接套用於日常開發的範例程式碼。
核心概念
1. 單繼承(Single Inheritance)
單繼承指的是子類別只繼承自 一個 父類別。這是最常見、最直觀的繼承方式。
class Animal:
"""所有動物的共通屬性與行為"""
def __init__(self, name: str):
self.name = name
def speak(self) -> str:
return "..."
class Dog(Animal):
"""Dog 繼承自 Animal,並覆寫 speak 方法"""
def speak(self) -> str:
return "汪汪!"
# 使用範例
my_dog = Dog("小黑")
print(my_dog.name) # -> 小黑
print(my_dog.speak())# -> 汪汪!
- 重點:子類別自動取得
Animal.__init__以及其他未被覆寫的方法。 super():在子類別想要呼叫父類別的__init__(或其他方法)時,使用super()可以避免硬編碼父類別名稱,提升未來維護彈性。
class Cat(Animal):
def __init__(self, name: str, lives: int = 9):
# 呼叫父類別的 __init__ 以設定 name
super().__init__(name)
self.lives = lives
def speak(self) -> str:
return "喵喵!"
my_cat = Cat("咪咪")
print(my_cat.name, my_cat.lives) # -> 咪咪 9
2. 多繼承(Multiple Inheritance)
Python 允許一個類別同時繼承自 多個 父類別。多繼承的威力在於可以把多個「功能」混合到同一個類別中,但也必須小心 方法解析順序(MRO) 的衝突問題。
class Flyer:
"""提供飛行能力的 mixin"""
def fly(self) -> str:
return "我會飛!"
class Swimmer:
"""提供游泳能力的 mixin"""
def swim(self) -> str:
return "我會游泳!"
class Duck(Flyer, Swimmer):
"""鴨子同時具備飛行與游泳能力"""
pass
daffy = Duck()
print(daffy.fly()) # -> 我會飛!
print(daffy.swim()) # -> 我會游泳!
2.1 方法解析順序(MRO)
當多個父類別中有同名方法時,Python 會依照 C3 線性化 演算法決定呼叫哪一個實作。可以透過 ClassName.__mro__ 或 help(ClassName) 觀察順序。
class A:
def hello(self):
return "Hello from A"
class B(A):
def hello(self):
return "Hello from B"
class C(A):
def hello(self):
return "Hello from C"
class D(B, C):
pass
print(D.__mro__) # (<class '__main__.D'>, <class '__main__.B'>,
# <class '__main__.C'>, <class '__main__.A'>,
# <class 'object'>)
d = D()
print(d.hello()) # -> Hello from B (先找 B,再找 C,最後才是 A)
小技巧:在多繼承中使用
super()時,一定要 讓父類別的__init__(或其他方法)也使用super(),才能形成完整的 MRO 鏈,避免某個父類別的初始化被跳過。
class LoggerMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) # 交給下一層處理
self.log = []
def record(self, msg: str):
self.log.append(msg)
class TimestampMixin:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.created_at = __import__('datetime').datetime.now()
class DataStore(LoggerMixin, TimestampMixin):
def __init__(self, data):
super().__init__() # 依照 MRO 呼叫兩個 mixin 的 __init__
self.data = data
store = DataStore([1, 2, 3])
print(store.created_at) # 顯示建立時間
print(store.log) # []
store.record("新增資料")
print(store.log) # ['新增資料']
3. isinstance 與 issubclass
在使用繼承時,我們常會需要檢查物件或類別的關係。
print(isinstance(my_dog, Animal)) # True
print(isinstance(my_dog, Dog)) # True
print(issubclass(Dog, Animal)) # True
print(issubclass(Duck, Flyer)) # True
4. 抽象基底類別(ABC)與介面概念
雖然 Python 沒有正式的「介面」概念,但可以透過 abc 模組建立抽象方法,強制子類別實作。
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(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
c = Circle(3)
print(c.area()) # 28.274333882308138
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記呼叫 super().__init__() |
子類別自行實作 __init__ 卻沒有呼叫 super(),導致父類別的屬性未被初始化。 |
在每個子類別的 __init__ 中 一定 使用 super().__init__(*args, **kwargs)。 |
| 多繼承的命名衝突 | 不同父類別提供同名方法,容易產生意外的行為。 | 使用 Mixin(只提供單一功能)並確保方法名稱具體化;或在子類別中明確覆寫並呼叫所需的父類別方法。 |
| MRO 不符合預期 | 由於 C3 線性化規則,某些繼承順序會產生不直觀的解析順序。 | 使用 ClassName.__mro__ 檢查;必要時調整繼承順序或重構為組合(Composition)而非多繼承。 |
| 過度使用多繼承 | 多層、多父的結構會讓程式碼難以閱讀、除錯。 | 儘量採用 單繼承 + Mixin 的方式,或改用 組合(將功能物件作為屬性注入)。 |
| 抽象類別忘記實作抽象方法 | 直接實例化抽象類別會拋出 TypeError。 |
在子類別實作所有 @abstractmethod,或使用 ABCMeta 檢查。 |
最佳實踐
- 使用
super():不論單繼承或多繼承,都應該使用super()來呼叫父類別,讓 MRO 正常運作。 - Mixin 命名慣例:以
Mixin為結尾(如LoggerMixin)讓閱讀者一眼辨識它是功能混入而非完整類別。 - 保持繼承樹淺層:盡量讓類別層級不超過 2~3 層,過深的繼承會增加耦合度。
- 文件化每個類別的責任:在類別說明字串中寫明「此類別負責什麼」與「它繼承了哪些行為」。
- 測試 MRO:在單元測試中加入
assert Class.__mro__ == expected_tuple,確保未因重構而改變解析順序。
實際應用場景
1. Web 框架的視圖(View)與混入(Mixin)
許多 Python Web 框架(如 Django、Flask)會提供 LoginRequiredMixin、PermissionRequiredMixin 等,用於在 類別型視圖 中快速加入授權檢查。
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views import View
class DashboardView(LoginRequiredMixin, View):
"""僅限已登入使用者能存取的儀表板"""
def get(self, request):
return HttpResponse("歡迎來到儀表板!")
2. GUI 元件的多功能組合
在桌面應用程式(如 PyQt、Tkinter)中,常需要把「可拖曳」與「可縮放」的行為分別寫成 Mixin,然後組合成最終元件。
class DraggableMixin:
def mousePressEvent(self, event):
self._drag_start = event.pos()
def mouseMoveEvent(self, event):
if event.buttons() & Qt.LeftButton:
self.move(self.pos() + event.pos() - self._drag_start)
class ResizableMixin:
def resizeEvent(self, event):
# 依需求調整子元件大小
super().resizeEvent(event) # 保持原有行為
class MyWindow(QWidget, DraggableMixin, ResizableMixin):
pass
3. 資料模型的領域驅動設計(DDD)
在大型系統中,領域模型 常使用抽象基底類別(如 Entity、ValueObject)提供共通的 ID、相等性比較,子類別只負責自己的業務邏輯。
class Entity(ABC):
@property
@abstractmethod
def id(self):
...
class User(Entity):
def __init__(self, user_id: int, name: str):
self._id = user_id
self.name = name
@property
def id(self):
return self._id
總結
- 繼承是 OOP 的基礎,讓我們可以在 單繼承 中重用父類別的行為,也能在 多繼承 中混合多個功能。
- 正確使用
super()、了解 MRO、以及善用 Mixin 與 抽象基底類別,是避免常見錯誤的關鍵。 - 在實務開發中,繼承常見於 Web 框架的視圖、GUI 元件的功能組合、以及領域模型的抽象層。
- 最重要的是:保持繼承樹簡潔、以組合取代過度的多繼承,才能寫出易於維護、可測試且具彈性的程式碼。
透過本篇的概念說明與實作範例,你已具備在 Python 專案中安全、有效地運用繼承的能力。接下來不妨把這些技巧套用到自己的程式碼庫,觀察可讀性與維護成本是否真的提升!祝開發愉快 🚀