本文 AI 產出,尚未審核

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 "喵喵!"

上述範例中,DogCat覆寫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 為抽象介面,LineCircle 必須實作 drawrender_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 使用 hasattrtry/except 包裝,或在介面層面明確定義。

最佳實踐

  1. 以介面為導向:先定義抽象基底類別或 Protocol,再讓具體類別實作。
  2. 保持方法簽名一致:多型的前提是「相同的訊息」—> 方法的參數與回傳型別盡量保持一致。
  3. 盡量避免 type()isinstance():讓物件自行決定如何回應訊息。
  4. 善用 @abstractmethod:在設計 API 時,明確告訴使用者哪些方法必須實作。
  5. 撰寫單元測試:測試多型容器或策略模式時,可利用 mock 物件驗證行為。

實際應用場景

  1. 插件系統(Plugin System)

    • 主程式只定義抽象介面(如 Plugin.execute()),第三方開發者可自行實作插件類別,程式在執行時自動載入並呼叫 execute,完全不需要修改核心程式碼。
  2. 資料序列化/反序列化

    • 透過多型,Serializer 抽象類別可以有 JsonSerializerXmlSerializerYamlSerializer 等實作,使用者只要切換實例即可改變輸出格式。
  3. 圖形使用者介面(GUI)元件

    • 所有 UI 元件(Button、Label、TextBox)繼承自 Widget,各自實作 drawhandle_event,主事件迴圈只需要遍歷 widgets 列表呼叫相同方法。
  4. 商業規則引擎

    • 不同的折扣、稅率、促銷策略可以抽象為 Rule 介面,透過多型把規則組合成一條流水線,簡化規則的新增與測試。
  5. 機器學習模型封裝

    • Model 抽象類別提供 fitpredict,不同框架(scikit‑learn、TensorFlow、PyTorch)各自實作,使用者只要切換模型物件即可在同一套程式碼中切換演算法。

總結

多型Python 物件導向 的核心力量,它讓我們能以 相同的介面 操作不同類別的物件,從而實現 低耦合、高彈性 的程式設計。透過 方法覆寫抽象基底類別、以及 duck typing,我們可以:

  • 建立可擴充的 API 與插件機制
  • 使用策略模式或工廠模式簡化業務邏輯
  • 在測試與實務開發中以 mock 物件取代實體依賴

掌握多型的概念與正確使用方式,將使你的 Python 程式碼更具可讀性、可維護性,也更容易在大型專案中保持清晰的架構。未來無論是開發 Web 應用、資料分析工具,或是 AI 模型平台,都能從多型的威力中受益,寫出 乾淨、彈性十足 的程式碼。祝你在 OOP 的旅程中玩得開心、寫得順手!