Python 物件導向程式設計(OOP)── 多型(Polymorphism)
簡介
在程式設計的世界裡,多型是物件導向最具威力的特性之一。它讓不同類別的物件可以「以相同的介面」執行相同的操作,從而大幅提升程式的彈性與可維護性。對於 Python 初學者而言,掌握多型不僅能寫出更簡潔的程式碼,也能在實務開發中減少重複與耦合,讓程式更容易擴充與測試。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,逐層帶領讀者深入了解 Python 中的多型,並以實際應用情境說明它在日常開發中的價值。
核心概念
1. 什麼是多型?
多型(Polymorphism)來源於希臘文「poly」(多) + 「morph」(形態)。在 OOP 中,它表示 同一訊息(method call)可以對不同類別的物件產生不同的行為。Python 透過繼承、抽象基底類別(ABC)與duck typing 三種機制來實現多型。
- 繼承:子類別覆寫父類別的方法,形成「方法多型」
- 抽象基底類別:使用
abc模組定義介面,強制子類別實作特定方法 - Duck typing:只要物件具備所需的方法,就能被當作該型別使用,無需明確的繼承關係
重點:在 Python 中,只要能呼叫相同的方法,就算是多型,不一定要透過繼承。
2. 方法覆寫(Method Overriding)
子類別可以重新定義(override)父類別的方法,讓同一個方法名稱在不同類別中有不同的實作。
class Animal:
def speak(self):
"""預設的叫聲"""
raise NotImplementedError("子類別必須實作 speak 方法")
class Dog(Animal):
def speak(self):
return "汪汪!"
class Cat(Animal):
def speak(self):
return "喵喵!"
上述範例中,Dog 與 Cat 都 覆寫 了 Animal.speak,呼叫 speak() 時會根據實例的實際類別回傳不同字串。
3. 抽象基底類別(ABC)
使用 abc.ABC 可以明確定義「必須實作」的抽象方法,讓開發者在設計介面時更具可讀性與安全性。
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
"""回傳圖形面積"""
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
此範例中,Shape 為抽象基底類別,area 為抽象方法,任何子類別若未實作 area,在實例化時會拋出 TypeError。
4. Duck Typing
Python 的「鴨子原則」主張:如果它走起路來像鴨子,叫起聲來像鴨子,那它就是鴨子。只要物件具備需要的方法,就能被當作該型別使用。
class Bird:
def fly(self):
print("鳥兒在天空飛翔")
class Airplane:
def fly(self):
print("飛機在高空巡航")
def let_it_fly(entity):
"""接受任何能 fly() 的物件"""
entity.fly()
let_it_fly(Bird()) # 輸出:鳥兒在天空飛翔
let_it_fly(Airplane()) # 輸出:飛機在高空巡航
let_it_fly 並不關心 entity 的真實類別,只要它有 fly 方法,就能正常運作。這就是 duck typing 帶來的彈性。
5. 多型與容器(Polymorphic Containers)
多型的威力在於可以把不同子類別的實例放入同一個容器(list、dict 等),然後統一操作。
shapes = [Rectangle(3, 4), Circle(5), Rectangle(2, 6)]
for s in shapes:
print(f"{s.__class__.__name__} 面積 = {s.area():.2f}")
輸出:
Rectangle 面積 = 12.00
Circle 面積 = 78.54
Rectangle 面積 = 12.00
不需要分別判斷每個物件的型別,直接呼叫 area() 即可。
程式碼範例
以下提供 5 個實用範例,從簡單到稍微進階,展示多型在日常開發中的應用。
範例 1:簡易的動物叫聲多型
class Animal:
def speak(self):
raise NotImplementedError
class Dog(Animal):
def speak(self):
return "汪汪!"
class Cat(Animal):
def speak(self):
return "喵喵!"
def make_sound(animals):
for a in animals:
print(a.speak())
pets = [Dog(), Cat(), Dog()]
make_sound(pets)
說明:
make_sound只要接受具備speak方法的物件,即可對不同動物產生正確叫聲。
範例 2:使用抽象基底類別定義繪圖介面
from abc import ABC, abstractmethod
class Drawable(ABC):
@abstractmethod
def draw(self, canvas):
"""在給定的 canvas 上繪製圖形"""
class Line(Drawable):
def __init__(self, start, end):
self.start = start
self.end = end
def draw(self, canvas):
canvas.draw_line(self.start, self.end)
class Circle(Drawable):
def __init__(self, center, radius):
self.center = center
self.radius = radius
def draw(self, canvas):
canvas.draw_circle(self.center, self.radius)
def render_scene(objects, canvas):
for obj in objects:
obj.draw(canvas)
# 假設有一個簡易的 Canvas 類別
class Canvas:
def draw_line(self, start, end):
print(f"畫線:{start} -> {end}")
def draw_circle(self, center, radius):
print(f"畫圓:中心 {center},半徑 {radius}")
scene = [Line((0, 0), (10, 10)), Circle((5, 5), 3)]
render_scene(scene, Canvas())
說明:
Drawable為抽象介面,Line與Circle必須實作draw,render_scene只需要知道「每個物件都有 draw」即可。
範例 3:Duck Typing 與檔案寫入
class JsonWriter:
def write(self, data):
import json
return json.dumps(data, ensure_ascii=False)
class CsvWriter:
def write(self, data):
import csv
from io import StringIO
output = StringIO()
writer = csv.writer(output)
writer.writerows(data)
return output.getvalue()
def export(data, writer):
"""接受任何具備 write 方法的 writer 物件"""
content = writer.write(data)
print(content)
sample = [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
export(sample, JsonWriter())
export([["Alice", 30], ["Bob", 25]], CsvWriter())
說明:
export不在乎 writer 的實際類別,只要它有write方法即可。這讓程式在未來加入 XML、YAML 等格式時,不需要改動export本身。
範例 4:策略模式(Strategy Pattern)結合多型
from abc import ABC, abstractmethod
class DiscountStrategy(ABC):
@abstractmethod
def calculate(self, amount):
"""回傳折扣後的金額"""
class NoDiscount(DiscountStrategy):
def calculate(self, amount):
return amount
class PercentageDiscount(DiscountStrategy):
def __init__(self, percent):
self.percent = percent
def calculate(self, amount):
return amount * (1 - self.percent / 100)
class FixedDiscount(DiscountStrategy):
def __init__(self, discount):
self.discount = discount
def calculate(self, amount):
return max(0, amount - self.discount)
class ShoppingCart:
def __init__(self, discount: DiscountStrategy):
self.items = []
self.discount = discount
def add_item(self, price):
self.items.append(price)
def total(self):
subtotal = sum(self.items)
return self.discount.calculate(subtotal)
# 使用不同折扣策略
cart1 = ShoppingCart(NoDiscount())
cart1.add_item(100)
cart1.add_item(200)
print("不打折總額:", cart1.total()) # 300
cart2 = ShoppingCart(PercentageDiscount(10))
cart2.add_item(100)
cart2.add_item(200)
print("九折總額:", cart2.total()) # 270.0
cart3 = ShoppingCart(FixedDiscount(50))
cart3.add_item(100)
cart3.add_item(200)
print("減 50 總額:", cart3.total()) # 250
說明:
DiscountStrategy為抽象策略,ShoppingCart只依賴calculate方法,讓不同折扣演算法能自由切換,這正是多型的典型應用。
範例 5:多型與單元測試(Mock)
from unittest.mock import MagicMock
class PaymentGateway:
def charge(self, amount):
"""實際向第三方支付平台收費"""
pass # 省略實作
def process_order(amount, gateway: PaymentGateway):
"""若付款成功回傳 True,否則回傳 False"""
try:
gateway.charge(amount)
return True
except Exception:
return False
# 測試時使用 mock 物件
mock_gateway = MagicMock()
mock_gateway.charge.return_value = None # 模擬成功
assert process_order(100, mock_gateway) is True
mock_gateway.charge.assert_called_once_with(100)
說明:測試環境中,我們不需要真正的
PaymentGateway實例,只要提供具備charge方法的 mock 物件即可,這也是 duck typing 的實務應用。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記呼叫 super().__init__() |
子類別覆寫 __init__ 後,若未呼叫父類別的建構子,父類別的屬性可能未被正確初始化。 |
在子類別 __init__ 開頭加入 super().__init__(…)。 |
| 抽象方法未實作卻直接實例化 | 直接 ClassName() 會拋出 TypeError。 |
使用抽象基底類別 (ABC) 強制實作,或在測試時使用 mock。 |
過度依賴 isinstance 判斷類別 |
會破壞多型的彈性,讓程式耦合度升高。 | 改用 duck typing 或 抽象介面,只檢查所需方法是否存在。 |
| 方法名稱拼寫錯誤 | Python 在執行時不會提前檢查,會在呼叫時才出錯。 | 透過 IDE、型別提示 (typing.Protocol) 或單元測試提前捕捉。 |
| 多型容器內的物件缺少必要屬性 | 迭代時呼叫不存在的方法會拋 AttributeError。 |
使用 hasattr 或 try/except 包裝,或在介面層面明確定義。 |
最佳實踐
- 以介面為導向:先定義抽象基底類別或
Protocol,再讓具體類別實作。 - 保持方法簽名一致:多型的前提是「相同的訊息」—> 方法的參數與回傳型別盡量保持一致。
- 盡量避免
type()或isinstance():讓物件自行決定如何回應訊息。 - 善用
@abstractmethod:在設計 API 時,明確告訴使用者哪些方法必須實作。 - 撰寫單元測試:測試多型容器或策略模式時,可利用 mock 物件驗證行為。
實際應用場景
插件系統(Plugin System)
- 主程式只定義抽象介面(如
Plugin.execute()),第三方開發者可自行實作插件類別,程式在執行時自動載入並呼叫execute,完全不需要修改核心程式碼。
- 主程式只定義抽象介面(如
資料序列化/反序列化
- 透過多型,
Serializer抽象類別可以有JsonSerializer、XmlSerializer、YamlSerializer等實作,使用者只要切換實例即可改變輸出格式。
- 透過多型,
圖形使用者介面(GUI)元件
- 所有 UI 元件(Button、Label、TextBox)繼承自
Widget,各自實作draw、handle_event,主事件迴圈只需要遍歷widgets列表呼叫相同方法。
- 所有 UI 元件(Button、Label、TextBox)繼承自
商業規則引擎
- 不同的折扣、稅率、促銷策略可以抽象為
Rule介面,透過多型把規則組合成一條流水線,簡化規則的新增與測試。
- 不同的折扣、稅率、促銷策略可以抽象為
機器學習模型封裝
Model抽象類別提供fit、predict,不同框架(scikit‑learn、TensorFlow、PyTorch)各自實作,使用者只要切換模型物件即可在同一套程式碼中切換演算法。
總結
多型 是 Python 物件導向 的核心力量,它讓我們能以 相同的介面 操作不同類別的物件,從而實現 低耦合、高彈性 的程式設計。透過 方法覆寫、抽象基底類別、以及 duck typing,我們可以:
- 建立可擴充的 API 與插件機制
- 使用策略模式或工廠模式簡化業務邏輯
- 在測試與實務開發中以 mock 物件取代實體依賴
掌握多型的概念與正確使用方式,將使你的 Python 程式碼更具可讀性、可維護性,也更容易在大型專案中保持清晰的架構。未來無論是開發 Web 應用、資料分析工具,或是 AI 模型平台,都能從多型的威力中受益,寫出 乾淨、彈性十足 的程式碼。祝你在 OOP 的旅程中玩得開心、寫得順手!