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 + obj2、obj1 * 3 |
| 比較 | __eq__、__lt__、__gt__… |
obj1 == obj2、obj1 < obj2 |
| 迭代 | __iter__、__next__ |
for x in obj、next(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__ 在成功時回傳目標值,失敗時拋 IndexError 或 KeyError。 |
__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 會失敗。 |
需要支援寫入或刪除時,同時實作對應的魔法方法。 |
最佳實踐
- 先寫測試:特殊方法往往牽涉到語法糖,使用
unittest或pytest驗證len()、[]、repr()等行為。 - 保持一致性:若
__repr__能eval,則__str__應該提供更簡潔的人類可讀版本,而不是相反。 - 盡量遵守 Pythonic 風格:
__len__、__getitem__、__iter__組合即可讓物件自然支援for...in、list()、sum()等內建函式。 - 文件化:在類別 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__ |
使集合可直接使用 in、len()、for x in coll 等語法,符合 Python 內建集合的行為。 |
範例:在網路爬蟲專案中,常會建立「URLQueue」類別。只要實作
__len__、__getitem__、__repr__,就能在除錯時直接印出排隊的 URL,或使用len(queue)檢查剩餘待抓取的數量,極大提升程式可讀性與維護性。
總結
特殊方法是 Python 讓自訂類別「看起來像內建型別」的核心機制。透過正確實作 __str__、__repr__、__len__、__getitem__(以及相關的 __setitem__、__iter__ 等),我們可以:
- 為物件提供清晰、可除錯的人類與機器可讀表示。
- 讓自訂容器自然支援
len()、索引、切片、迭代等常見操作。 - 在程式碼中保持 Pythonic 的風格,減少額外包裝函式,提升可讀性與可維護性。
在實務開發中,將這些魔法方法納入類別設計,是打造 易用、易除錯、易擴充 API 的關鍵。建議在撰寫每個新類別時,先思考該類別是否應該支援哪些語法糖,然後以測試驅動的方式實作對應的 dunder 方法,這樣既能確保行為正確,也能在未來的重構中保持一致性。
祝你在 Python 的物件導向世界裡,玩得開心、寫得優雅! 🚀