本文 AI 產出,尚未審核

Python 物件導向程式設計(OOP)

主題:特殊方法(dunder methods)


簡介

在 Python 中,幾乎所有的功能都是透過「方法」來實作,而「特殊方法」(又稱 dunder,double underscore) 則是語言層面的魔法入口。只要在自訂類別中正確實作 __str____repr____len____getitem__ 等方法,Python 便會在適當的情境自動呼叫它們,讓自訂物件的行為與內建類型無縫銜接。

對於 初學者,了解這些方法可以讓程式碼更具可讀性與可除錯性;對 中級開發者,則能利用它們打造符合 Pythonic 風格的 API、資料結構或 DSL(Domain‑Specific Language)。本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,一直到實務應用場景,完整呈現特殊方法的威力與使用方式。


核心概念

1. 為什麼需要特殊方法?

目的 常見的 dunder 方法 觸發時機
字串表示 __str____repr__ print(obj)repr(obj)、交互式直譯器
容器行為 __len____getitem____setitem____delitem__ len(obj)obj[i]for x in obj
算術運算 __add____sub____mul__ obj1 + obj2obj1 * 3
比較 __eq____lt____gt__ obj1 == obj2obj1 < obj2
迭代 __iter____next__ for x in objnext(iter_obj)

特殊方法的命名規則是兩個底線開頭、兩個底線結尾(__method__)。Python 內部在執行相應語法時會自動搜尋同名的方法,若找到就執行;若找不到則拋出 AttributeError 或使用預設行為。


2. __str____repr__

  • __str__:提供「人類可讀」的字串表示,通常用於輸出、日誌。
  • __repr__:提供「開發者可讀」且盡可能能 eval 回原物件的字串,主要用於除錯與交互式環境。

範例 1:基本實作

class Point:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        # 盡量讓 eval(repr(p)) 產生相同的物件
        return f"Point({self.x!r}, {self.y!r})"

    def __str__(self) -> str:
        # 人類友善的輸出
        return f"({self.x}, {self.y})"


p = Point(3.5, -2)
print(p)          # 呼叫 __str__ → (3.5, -2)
print(repr(p))    # 呼叫 __repr__ → Point(3.5, -2)

小技巧:在 __repr__ 中使用 !r 讓每個屬性自行呼叫 repr,可避免類型不一致的問題。


3. __len__ – 定義容器長度

len(obj) 會呼叫 obj.__len__(),返回一個 非負整數。如果物件不支援長度,應拋出 TypeError

範例 2:自訂 Stack

class Stack:
    def __init__(self):
        self._items = []

    def push(self, item):
        self._items.append(item)

    def pop(self):
        return self._items.pop()

    def __len__(self) -> int:
        # 回傳內部列表的長度
        return len(self._items)


s = Stack()
s.push(1)
s.push(2)
print(len(s))     # 2

注意__len__ 必須回傳 int,若回傳 float 或負值會在 len() 呼叫時產生 TypeError


4. __getitem__ – 支援索引與切片

實作 __getitem__(self, key) 後,物件即可使用 obj[key] 取得值。key 可以是整數、切片 (slice) 或自訂類型(如 tuple 用於多維索引)。

範例 3:簡易的二維矩陣

class Matrix:
    def __init__(self, rows: int, cols: int, fill=0):
        self._data = [[fill for _ in range(cols)] for _ in range(rows)]

    def __getitem__(self, idx):
        # 支援 m[i] 取第 i 列,或 m[i, j] 取第 i 列第 j 欄
        if isinstance(idx, tuple):
            row, col = idx
            return self._data[row][col]
        return self._data[idx]

    def __setitem__(self, idx, value):
        if isinstance(idx, tuple):
            row, col = idx
            self._data[row][col] = value
        else:
            self._data[idx] = value

    def __repr__(self):
        return f"Matrix({self._data})"


m = Matrix(3, 3, fill=0)
m[0, 1] = 5
print(m[0, 1])    # 5
print(m[0])       # [0, 5, 0]

延伸:若要支援 for x in obj:,只需要再實作 __iter__ 或讓 __getitem__ 在索引超出範圍時拋出 IndexError


5. 結合多個特殊方法:自訂「可切片的序列」

範例 4:只讀的範圍 (Range-like) 物件

class ReadOnlyRange:
    def __init__(self, start: int, stop: int, step: int = 1):
        self.start = start
        self.stop = stop
        self.step = step
        self._len = max(0, (stop - start + (step - 1)) // step)

    def __len__(self) -> int:
        return self._len

    def __getitem__(self, index):
        if isinstance(index, slice):
            # 產生新的 ReadOnlyRange 作為切片結果
            start, stop, step = index.indices(self._len)
            new_start = self.start + start * self.step
            new_step = self.step * step
            new_stop = new_start + (stop - start) * self.step
            return ReadOnlyRange(new_start, new_stop, new_step)
        if index < 0:
            index += self._len
        if not 0 <= index < self._len:
            raise IndexError('index out of range')
        return self.start + index * self.step

    def __repr__(self) -> str:
        return f"ReadOnlyRange({self.start}, {self.stop}, {self.step})"


r = ReadOnlyRange(0, 10, 2)   # 0,2,4,6,8
print(len(r))                 # 5
print(r[2])                   # 4
print(r[1:4])                 # ReadOnlyRange(2, 8, 2)

此範例展示了如何同時使用 __len____getitem__(支援切片)以及 __repr__,讓自訂類別在大多數序列操作上行為一致。


常見陷阱與最佳實踐

陷阱 說明 建議的做法
忘記回傳正確型別 __len__ 回傳 float__getitem__ 回傳 None 會導致錯誤。 確保 __len__ 回傳 int__getitem__ 在成功時回傳目標值,失敗時拋 IndexErrorKeyError
__repr__ 不具備可 eval 的特性 雖非必須,但若 repr 不能重建物件,除錯時會失去資訊。 盡量讓 __repr__ 回傳可直接 eval 的字串,或在文件中說明其限制。
切片返回同類型物件時忘記處理 step slice.indices 會自行計算正確的 start/stop/step,若直接使用 slice.start 可能產生錯誤。 使用 slice.indices(len(self)) 取得正規化的索引,並依此建立新物件。
__getitem__ 中直接使用 list[index] 而未捕捉 IndexError 若自訂容器內部結構改變,外部呼叫仍會拋出未預期的錯誤。 先自行檢查邊界或使用 try/except 包裝,確保錯誤訊息清晰。
忘記實作 __setitem__ / __delitem__ 若只實作 __getitem__,使用 obj[i] = x 會失敗。 需要支援寫入或刪除時,同時實作對應的魔法方法。

最佳實踐

  1. 先寫測試:特殊方法往往牽涉到語法糖,使用 unittestpytest 驗證 len()、[]、repr() 等行為。
  2. 保持一致性:若 __repr__eval,則 __str__ 應該提供更簡潔的人類可讀版本,而不是相反。
  3. 盡量遵守 Pythonic 風格__len____getitem____iter__ 組合即可讓物件自然支援 for...inlist()sum() 等內建函式。
  4. 文件化:在類別 docstring 中說明哪些特殊方法已實作,以及它們的行為限制(例如「只讀」或「不支援負索引」)。

實際應用場景

場景 使用的特殊方法 為什麼適合
自訂資料模型(如 ORM 的模型物件) __repr____str____len__(計算關聯筆數) 讓開發者在除錯或日誌中直接看到完整資訊,len(model) 可直接取得關聯集合大小。
實作虛擬檔案系統 __getitem____len____iter__ 讓檔案列表像普通 list 一樣使用 for f in vfs:len(vfs),提升 API 一致性。
演算法教學用的「序列」 __getitem__(支援切片) 讓學生可以直接使用切片檢查子序列,減少額外的 helper 函式。
數值計算套件(如向量、矩陣) __repr____str____len____getitem____setitem__ 提供直觀的顯示、支援 len(vec)、索引取值與賦值,使其與 numpy.ndarray 的使用感受相近。
自訂集合類別(如多重集合、優先佇列) __len____contains____iter____repr__ 使集合可直接使用 inlen()for x in coll 等語法,符合 Python 內建集合的行為。

範例:在網路爬蟲專案中,常會建立「URLQueue」類別。只要實作 __len____getitem____repr__,就能在除錯時直接印出排隊的 URL,或使用 len(queue) 檢查剩餘待抓取的數量,極大提升程式可讀性與維護性。


總結

特殊方法是 Python 讓自訂類別「看起來像內建型別」的核心機制。透過正確實作 __str____repr____len____getitem__(以及相關的 __setitem____iter__ 等),我們可以:

  • 為物件提供清晰、可除錯的人類與機器可讀表示。
  • 讓自訂容器自然支援 len()、索引、切片、迭代等常見操作。
  • 在程式碼中保持 Pythonic 的風格,減少額外包裝函式,提升可讀性與可維護性。

在實務開發中,將這些魔法方法納入類別設計,是打造 易用、易除錯、易擴充 API 的關鍵。建議在撰寫每個新類別時,先思考該類別是否應該支援哪些語法糖,然後以測試驅動的方式實作對應的 dunder 方法,這樣既能確保行為正確,也能在未來的重構中保持一致性。

祝你在 Python 的物件導向世界裡,玩得開心、寫得優雅! 🚀