目錄
- 什麼是 Dunder Method?
- 為什麼這樣設計?(Python 的協定哲學)
- 物件生命週期
- 物件表示
- 比較與雜湊
- 算術運算
- 容器與迭代
- Context Manager(重點)
- 屬性存取
- 可呼叫物件(實戰)
- Descriptor(實戰)
- 類別行為
- 異步版本
- 完整對照速查表
- 最佳實踐
- 常見問題
- 總結
什麼是 Dunder Method?
Dunder method = Double UNDERscore method,指 Python 中以「雙底線開頭、雙底線結尾」的特殊方法:
__init__ __str__ __repr__ __eq__
__enter__ __exit__ __call__ __getitem__
__add__ __len__ __iter__ __hash__
別名
| 名稱 | 來源 | 備註 |
|---|---|---|
| Dunder method | 社群口語 | Double UNDERscore 縮寫,最常用 |
| Magic method | 早期教學 | 強調「魔術般」的自動觸發 |
| Special method | Python 官方文件 | Data Model 章節用語 |
三個名稱指的是同一件事。
數量與重要性
Python 有 80+ 個 dunder method,但日常使用的核心約 20-30 個。它們不是「進階技巧」——而是 Python 物件模型的底層機制:
a + b # → a.__add__(b)
a[0] # → a.__getitem__(0)
len(a) # → a.__len__()
str(a) # → a.__str__()
with a: # → a.__enter__() / a.__exit__()
a() # → a.__call__()
a == b # → a.__eq__(b)
for x in a: # → a.__iter__() + __next__()
所有 Python 內建語法糖背後,都是某個 dunder method 在運作。
為什麼這樣設計?(Python 的協定哲學)
對比 Java:interface vs dunder method
Java:要讓物件可以被 for 迭代,必須宣告實作 Iterable<T> interface:
public class MyList implements Iterable<Integer> {
@Override
public Iterator<Integer> iterator() { ... }
}
Python:只要實作 __iter__ 方法,就自動可以被 for 用:
class MyList:
def __iter__(self):
...
沒有 implements、沒有繼承、沒有宣告——只要「有 __iter__ 方法」這個事實,Python 就認得它能被迭代。這就是 duck typing:「If it walks like a duck and quacks like a duck, it is a duck」。
Dunder method 就是「協定(Protocol)」
Python 把語言內建語法的行為外包給物件本身:
語法 / 內建函式 ←→ 對應的 dunder method(協定)
────────────────────────────────────────────────────────────
a + b ←→ __add__
a < b ←→ __lt__
a == b ←→ __eq__
len(a) ←→ __len__
hash(a) ←→ __hash__
str(a) ←→ __str__
with a: ←→ __enter__ / __exit__
for x in a: ←→ __iter__ / __next__
a[0] ←→ __getitem__
a() ←→ __call__
設計優點:
- 語法統一:自訂類別與內建型別行為一致
- 無需繼承:不必為了「可迭代」而強制繼承某個基類
- 解耦:型別不必預知所有用途,只要實作對應協定即可
★ Insight ─────────────────────────────────────
Dunder method 是 Python「優雅」的根源。Java 的 iterator() 是「方法」,Python 的 __iter__ 是「協定」——前者要繼承宣告,後者只要實作。這個差異讓 Python 的內建語法可以無痛延伸到任何自訂類別:你寫一個 MyList 類別,加上 __getitem__,立刻可以 MyList()[0] 而不必繼承 list。這也是為什麼 NumPy 的 ndarray、Pandas 的 DataFrame 行為能跟內建型別一樣自然。
─────────────────────────────────────────────────
物件生命週期
__new__ vs __init__
| 方法 | 角色 | 何時呼叫 |
|---|---|---|
__new__(cls, ...) |
建構子:負責「建立」物件,回傳新實例 | 物件建立前 |
__init__(self, ...) |
初始化器:負責「初始化」已建立的物件 | 物件建立後 |
class Foo:
def __new__(cls, *args, **kwargs):
print(f"__new__ called,cls={cls}")
instance = super().__new__(cls)
return instance
def __init__(self, value):
print(f"__init__ called,value={value}")
self.value = value
foo = Foo(42)
# __new__ called,cls=<class '__main__.Foo'>
# __init__ called,value=42
99% 情況只需要寫 __init__。__new__ 用於:
- 不可變型別繼承(如繼承
int、tuple、str) - 單例模式(Singleton)
- Metaclass 自訂類別建立流程(深層用法)
__del__:解構子(多數情況不該用)
class Resource:
def __del__(self):
print("Resource cleaned up")
為什麼不該用:
- 呼叫時機不保證:CPython 用引用計數即時呼叫,PyPy 等實作不一定
- 循環引用會延遲呼叫:被 GC 處理才會跑
- 直譯器結束時不一定執行
⚠️ 清理資源請用 Context Manager(
__enter__/__exit__),不要用__del__。後者只該用於極少數場景(如 weakref callback)。
物件表示
__str__ vs __repr__(最常被搞混)
| 方法 | 觸發來源 | 對象 | 風格 |
|---|---|---|---|
__str__ |
str(obj) / print(obj) / f-string {obj} |
使用者 | 易讀、自然語言 |
__repr__ |
repr(obj) / REPL 直接顯示 / [obj] 印出時 |
開發者 | 明確、最好能 eval 還原 |
from datetime import date
class Person:
def __init__(self, name, birthday):
self.name = name
self.birthday = birthday
def __str__(self):
return f"{self.name}({self.birthday})"
def __repr__(self):
return f"Person(name={self.name!r}, birthday={self.birthday!r})"
p = Person("Alice", date(1990, 1, 1))
print(p) # → Alice(1990-01-01) ← __str__
print(repr(p)) # → Person(name='Alice', birthday=datetime.date(1990, 1, 1))
print([p]) # → [Person(name='Alice', ...)] ← list 內部用 __repr__
決策樹
寫類別時要不要實作?
│
├── 一定要寫 __repr__ ← debug 救命
│ └── 慣例:repr 看起來像「能 eval 還原物件的呼叫」
│
└── __str__ 是選配
└── 不寫的話,str() 預設用 __repr__
黃金法則:永遠寫
__repr__,視需要寫__str__。沒有__repr__的類別在 debug 時會看到<__main__.Foo object at 0x10a3b4d50>,極為痛苦。
__format__
f-string 的格式化規範背後:
class Money:
def __init__(self, amount):
self.amount = amount
def __format__(self, spec):
if spec == "ntd":
return f"NT$ {self.amount:,}"
if spec == "usd":
return f"$ {self.amount / 30:.2f}"
return str(self.amount)
m = Money(1500000)
print(f"{m:ntd}") # → NT$ 1,500,000
print(f"{m:usd}") # → $ 50000.00
比較與雜湊
比較運算符
| 運算符 | dunder method |
|---|---|
== |
__eq__ |
!= |
__ne__(預設取 __eq__ 反向) |
< |
__lt__ |
<= |
__le__ |
> |
__gt__ |
>= |
__ge__ |
class Version:
def __init__(self, major, minor):
self.major = major
self.minor = minor
def __eq__(self, other):
return (self.major, self.minor) == (other.major, other.minor)
def __lt__(self, other):
return (self.major, self.minor) < (other.major, other.minor)
用 @functools.total_ordering 偷懶
實作所有 6 個比較方法很煩,只實作 __eq__ 與 __lt__ 即可:
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor):
self.major, self.minor = major, minor
def __eq__(self, other):
return (self.major, self.minor) == (other.major, other.minor)
def __lt__(self, other):
return (self.major, self.minor) < (other.major, other.minor)
# 自動補齊 <=, >, >=, !=
v1 = Version(1, 0)
v2 = Version(2, 0)
print(v1 < v2) # True
print(v1 >= v2) # False ← 自動產生
__eq__ 與 __hash__ 的契約
鐵律:如果你定義了 __eq__,物件預設變成 unhashable(不能放進 set 或當 dict key)。要恢復 hashable,必須同時定義 __hash__:
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
def __eq__(self, other):
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y)) # 與 __eq__ 邏輯一致
s = {Point(1, 2), Point(1, 2)} # 因為 hash 相同 + eq 相等 → 合併成一個
print(len(s)) # 1
契約:a == b 必須蘊含 hash(a) == hash(b)。不遵守這條會在 dict/set 裡遇到難找的 bug。
算術運算
三種變體:基本、in-place、reversed
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __add__(self, other): # a + b
return Vector(self.x + other.x, self.y + other.y)
def __iadd__(self, other): # a += b
self.x += other.x
self.y += other.y
return self
def __radd__(self, other): # b + a,當 b 不會 __add__(a) 時
return self.__add__(other)
為什麼要有 __radd__?
v = Vector(1, 2)
v + 3 # 呼叫 v.__add__(3)
3 + v # 先嘗試 (3).__add__(v) → NotImplemented
# → 再嘗試 v.__radd__(3) → 成功
沒有 __radd__,3 + v 會 TypeError。NumPy 能讓 2 * arr 與 arr * 2 都成立,就是靠 __rmul__。
完整算術運算符
| 運算符 | dunder | 反向 | in-place |
|---|---|---|---|
+ |
__add__ |
__radd__ |
__iadd__ |
- |
__sub__ |
__rsub__ |
__isub__ |
* |
__mul__ |
__rmul__ |
__imul__ |
/ |
__truediv__ |
__rtruediv__ |
__itruediv__ |
// |
__floordiv__ |
__rfloordiv__ |
__ifloordiv__ |
% |
__mod__ |
__rmod__ |
__imod__ |
** |
__pow__ |
__rpow__ |
__ipow__ |
@ |
__matmul__ |
__rmatmul__ |
__imatmul__ |
&、` |
、^` |
__and__、__or__、__xor__ |
r/i 變體 |
-x(一元) |
__neg__ |
— | — |
abs(x) |
__abs__ |
— | — |
@是 Python 3.5+ 的矩陣乘法運算符(NumPy 用)。
容器與迭代
容器協定
| 語法 | dunder method | 說明 |
|---|---|---|
len(a) |
__len__ |
回傳長度(int) |
a[i] |
__getitem__(i) |
取元素 |
a[i] = v |
__setitem__(i, v) |
設元素 |
del a[i] |
__delitem__(i) |
刪元素 |
x in a |
__contains__(x) |
包含檢查 |
for x in a: |
__iter__ |
迭代 |
reversed(a) |
__reversed__ |
反向迭代 |
class Bag:
def __init__(self, items):
self._items = list(items)
def __len__(self):
return len(self._items)
def __getitem__(self, i):
return self._items[i]
def __contains__(self, x):
return x in self._items
bag = Bag([1, 2, 3])
print(len(bag)) # 3
print(bag[0]) # 1
print(2 in bag) # True
for item in bag: # 即使沒寫 __iter__ 也能用!
print(item)
小驚喜:只要實作
__getitem__(i)並能處理連續整數索引,for迴圈會自動使用它。這是 Python 的「舊式 iterator 協定」。但更明確的做法是直接實作__iter__。
迭代協定:__iter__ vs __next__
兩者不同:
| 方法 | 角色 | 回傳 |
|---|---|---|
__iter__ |
Iterable(可被迭代) | 一個 iterator 物件 |
__next__ |
Iterator(迭代器本身) | 下一個值,沒有時 raise StopIteration |
class CountDown:
def __init__(self, start):
self.start = start
def __iter__(self):
return self # 自己就是 iterator
def __next__(self):
if self.start <= 0:
raise StopIteration
self.start -= 1
return self.start + 1
for n in CountDown(3):
print(n) # 3, 2, 1
更 Pythonic 的寫法:用 generator(yield)自動實作這兩個方法:
class CountDown:
def __init__(self, start):
self.start = start
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1
Context Manager(重點)
這是你最初提到的主題,所以詳細展開。
with 語法糖背後做了什麼
with open("file.txt") as f:
data = f.read()
等同於:
f = open("file.txt")
try:
f = f.__enter__() # ← 啟動:取得資源
data = f.read()
finally:
f.__exit__(*sys.exc_info()) # ← 清理:保證執行
核心價值:保證清理動作一定執行——即使區塊內 raise 例外、return、break。
自訂 Context Manager 類別
class Timer:
def __enter__(self):
import time
self.start = time.perf_counter()
return self # 通常回傳自己
def __exit__(self, exc_type, exc_value, traceback):
import time
elapsed = time.perf_counter() - self.start
print(f"Elapsed: {elapsed:.3f}s")
# 不回傳 True → 不抑制例外
with Timer():
sum(range(10_000_000))
# Elapsed: 0.234s
__exit__ 的三個參數與例外處理
def __exit__(self, exc_type, exc_value, traceback):
...
| 參數 | 內容 |
|---|---|
exc_type |
例外類別(如 ValueError),無例外時為 None |
exc_value |
例外實例 |
traceback |
traceback 物件 |
回傳值決定例外是否被抑制:
__exit__ 回傳 |
行為 |
|---|---|
False / None(預設) |
例外繼續往外傳 |
True |
例外被抑制,with 區塊外不會看到 |
例外處理流程圖
進入 with 區塊
│
▼
__enter__()
│
▼
執行 with 內部程式
│
▼
┌────────────────────┐
│ 有例外 ? │
└────────────────────┘
✅ 是 ❌ 否
│ │
▼ ▼
__exit__( __exit__(
exc_type, None,
exc_value, None,
traceback None
) )
│ │
▼ ▼
回傳 True ? 結束
✅ 抑制例外
❌ 例外繼續傳出
實戰範例 1:DB Transaction
class Transaction:
def __init__(self, conn):
self.conn = conn
def __enter__(self):
self.conn.execute("BEGIN")
return self.conn
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
self.conn.execute("COMMIT")
else:
self.conn.execute("ROLLBACK")
print(f"Transaction rolled back due to: {exc_value}")
# 回傳 None → 例外繼續傳給外層
# 使用
with Transaction(conn) as cur:
cur.execute("UPDATE users SET balance = balance - 100 WHERE id = 1")
cur.execute("UPDATE users SET balance = balance + 100 WHERE id = 2")
# 區塊內成功 → 自動 COMMIT
# 區塊內 raise → 自動 ROLLBACK,例外繼續傳出
實戰範例 2:暫時改變狀態(自動還原)
import os
class chdir:
"""暫時切換工作目錄,離開 with 自動還原"""
def __init__(self, target):
self.target = target
self.original = None
def __enter__(self):
self.original = os.getcwd()
os.chdir(self.target)
return self.target
def __exit__(self, *args):
os.chdir(self.original)
with chdir("/tmp"):
# 在這個區塊內,cwd 是 /tmp
do_something()
# 離開後自動回到原本的目錄,即使區塊內 raise
@contextmanager 裝飾器(更簡潔)
contextlib.contextmanager 用 generator 寫 context manager,不必寫類別:
from contextlib import contextmanager
@contextmanager
def timer():
import time
start = time.perf_counter()
try:
yield # ← yield 等同於 __enter__ 結束
finally:
elapsed = time.perf_counter() - start
print(f"Elapsed: {elapsed:.3f}s")
# finally 等同於 __exit__
with timer():
sum(range(10_000_000))
結構對照:
| 類別寫法 | @contextmanager 寫法 |
|---|---|
__enter__ 內容 |
yield 之前 |
yield 的值 |
__enter__ 回傳值(給 as x: 用) |
__exit__ 清理 |
yield 之後(建議放 finally) |
| 抑制例外 | try/except + 不重新 raise |
何時用哪個?——簡單清理用
@contextmanager,需要狀態或多方法用類別。
contextlib.ExitStack(動態組合多個 context manager)
當「context manager 數量在執行時才知道」時,用 ExitStack 動態管理:
from contextlib import ExitStack
def open_many(filenames):
with ExitStack() as stack:
files = [stack.enter_context(open(f)) for f in filenames]
# 處理所有檔案...
return [f.read() for f in files]
# 離開時所有 file 都會被正確關閉,即使中途有例外
同時開多個 context manager
# 寫法 1:括號分行(Python 3.10+)
with (
open("a.txt") as a,
open("b.txt") as b,
Timer(),
):
...
# 寫法 2:逗號(任何版本)
with open("a.txt") as a, open("b.txt") as b:
...
異步版本:__aenter__ / __aexit__
class AsyncDBConnection:
async def __aenter__(self):
self.conn = await create_connection()
return self.conn
async def __aexit__(self, exc_type, exc_value, traceback):
await self.conn.close()
async def main():
async with AsyncDBConnection() as conn:
await conn.query("SELECT * FROM users")
規則:所有 dunder method 加上 a 前綴(async)就是異步版本:__aenter__、__aexit__、__aiter__、__anext__。
屬性存取
__getattr__ vs __getattribute__(最容易踩坑)
| 方法 | 何時觸發 |
|---|---|
__getattribute__(name) |
每次 屬性查詢都觸發(包括存在的屬性) |
__getattr__(name) |
只有在正常查詢失敗時 才觸發 |
class Lazy:
def __init__(self):
self.x = 1
def __getattr__(self, name):
# 只在 self.x 找不到時才會跑這裡
print(f"__getattr__ called: {name}")
return f"<dynamic: {name}>"
obj = Lazy()
print(obj.x) # 1 ← __getattr__ 不觸發(x 存在)
print(obj.unknown) # <dynamic: unknown> ← __getattr__ 觸發
建議:絕大多數情況用
__getattr__,不要動__getattribute__——後者一旦寫錯就會無限遞迴或破壞所有屬性查詢。
__setattr__ 的無限遞迴陷阱
class Bad:
def __setattr__(self, name, value):
self.name = value # ❌ 又觸發 __setattr__ → 無限遞迴
class Good:
def __setattr__(self, name, value):
# ✅ 直接操作 __dict__ 繞過 __setattr__
super().__setattr__(name, value)
# 或
# self.__dict__[name] = value
實際應用:自動 type 轉換
class TypedConfig:
def __setattr__(self, name, value):
if name == "port":
value = int(value)
super().__setattr__(name, value)
config = TypedConfig()
config.port = "8080" # 字串
print(config.port, type(config.port)) # 8080 <class 'int'>
可呼叫物件(實戰)
基本:用 __call__ 讓實例可以被呼叫
class Greeter:
def __init__(self, greeting):
self.greeting = greeting
def __call__(self, name):
return f"{self.greeting}, {name}!"
hello = Greeter("Hello")
print(hello("World")) # Hello, World!
print(hello.__class__) # <class '__main__.Greeter'> ← 它仍是物件
實例像函式一樣呼叫,但保有狀態——這就是 __call__ 的核心價值。
實戰:實作 Memoize 快取裝飾器
from functools import wraps
class Memoize:
"""快取函式呼叫結果。可以用 stats() 查詢命中率"""
def __init__(self, func):
self.func = func
self.cache = {}
self.hits = 0
self.misses = 0
wraps(func)(self) # 保留原函式的 __name__、__doc__
def __call__(self, *args):
if args in self.cache:
self.hits += 1
return self.cache[args]
self.misses += 1
result = self.func(*args)
self.cache[args] = result
return result
def stats(self):
total = self.hits + self.misses
rate = self.hits / total if total else 0
return f"Hits: {self.hits}, Misses: {self.misses}, Hit rate: {rate:.1%}"
def clear(self):
self.cache.clear()
self.hits = self.misses = 0
@Memoize
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(30)) # 832040
print(fib.stats()) # Hits: 28, Misses: 31, Hit rate: 47.5%
fib.clear() # 清快取
為什麼用類別而不是 closure?——因為類別實例可以保有額外狀態(hits、misses)並提供 stats()、clear() 等附屬方法。純 closure 寫法做不到。
實戰:有狀態的計數器
class Counter:
def __init__(self):
self.count = 0
def __call__(self):
self.count += 1
return self.count
next_id = Counter()
print(next_id()) # 1
print(next_id()) # 2
print(next_id()) # 3
print(next_id.count) # 3 ← 仍能查詢狀態
★ Insight ─────────────────────────────────────
__call__ 的設計哲學:函式與物件在 Python 中本來就沒有清楚界線——函式是 function 類別的實例。__call__ 把這個事實「公開化」:你的物件可以同時擁有「函式般的呼叫語義」與「物件的狀態與方法」。Flask、Click、PyTorch 的 nn.Module 都重度使用這個模式——例如 PyTorch 的 model(input) 看似函式呼叫,實際上是 model.__call__(input),內部會做 hook、梯度追蹤等狀態管理。
─────────────────────────────────────────────────
Descriptor(實戰)
Descriptor 是 Python 中最強大也最少人理解的機制——但 @property、@staticmethod、@classmethod 全都靠它。
三個 dunder method
| 方法 | 觸發 |
|---|---|
__get__(self, instance, owner) |
讀取屬性 |
__set__(self, instance, value) |
寫入屬性 |
__delete__(self, instance) |
刪除屬性 |
最簡 descriptor
class Logged:
def __get__(self, instance, owner):
print(f"GET: {instance}")
return 42
class Foo:
x = Logged() # ← x 是 Logged 實例(descriptor)
f = Foo()
print(f.x)
# GET: <__main__.Foo object at ...>
# 42
@property 是 descriptor 的應用
# 你寫:
class C:
@property
def value(self):
return self._value
# Python 內部等同於:
class C:
def get_value(self):
return self._value
value = property(get_value) # ← property 是 descriptor 類別
實戰範例 1:Lazy Property(昂貴計算只算一次)
class LazyProperty:
"""只在第一次存取時計算,結果快取在實例上"""
def __init__(self, func):
self.func = func
self.name = func.__name__
def __set_name__(self, owner, name):
# 自動取得屬性名(Python 3.6+)
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
# 計算結果並寫到 instance.__dict__
# 下次存取時,__dict__ 優先 → 不會再觸發 descriptor
value = self.func(instance)
instance.__dict__[self.name] = value
return value
class DataAnalyzer:
def __init__(self, dataset):
self.dataset = dataset
@LazyProperty
def expensive_stats(self):
print("Computing... (slow)")
import time; time.sleep(2)
return {"mean": sum(self.dataset) / len(self.dataset)}
a = DataAnalyzer([1, 2, 3, 4, 5])
print(a.expensive_stats) # Computing... (slow) → {'mean': 3.0}
print(a.expensive_stats) # 直接回傳,無 "Computing"
核心技巧:
__set_name__讓 descriptor 自動知道自己的「屬性名」是什麼,這樣才能把快取結果寫進instance.__dict__[self.name]。
實戰範例 2:型別驗證屬性
class Typed:
"""強制屬性必須是某型別,否則 TypeError"""
def __init__(self, expected_type):
self.expected_type = expected_type
def __set_name__(self, owner, name):
self.name = f"_{name}" # 實際值存在 _name 避免遞迴
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.name)
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError(f"{self.name} 必須是 {self.expected_type.__name__}")
setattr(instance, self.name, value)
class User:
name = Typed(str)
age = Typed(int)
def __init__(self, name, age):
self.name = name
self.age = age
u = User("Alice", 30)
print(u.name, u.age) # Alice 30
u.age = "thirty"
# TypeError: _age 必須是 int
Data Descriptor vs Non-data Descriptor
| 類型 | 定義 | 優先順序 |
|---|---|---|
| Data descriptor | 有 __set__ 或 __delete__ |
比 instance.__dict__ 高 |
| Non-data descriptor | 只有 __get__ |
比 instance.__dict__ 低 |
這個差異決定了 LazyProperty 為什麼能 work:LazyProperty 只有 __get__(non-data),所以把結果寫進 instance.__dict__ 後,下次存取會優先讀 dict,descriptor 不再被觸發。
★ Insight ─────────────────────────────────────
Descriptor 是 Python 的「屬性中介層」。當你寫 obj.attr 時,Python 不是單純去 obj.__dict__ 找——而是經過一連串查詢:type(obj).__mro__ 上有沒有 data descriptor → obj.__dict__ → type(obj).__mro__ 上的 non-data descriptor → __getattr__ fallback。這個查詢順序就是 @property、@classmethod、ORM 欄位定義(models.CharField())能運作的底層機制。理解 descriptor 後,你看 Django Model 那行 name = models.CharField() 就會明白——CharField 是個 descriptor,它攔截了 instance.name 的讀寫。
─────────────────────────────────────────────────
類別行為
__init_subclass__:子類自動處理
當子類被定義時自動執行(不是實例化時):
class Plugin:
registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Plugin.registry[cls.__name__] = cls
print(f"Registered: {cls.__name__}")
class FooPlugin(Plugin):
pass
# Registered: FooPlugin ← 類別建立時觸發
class BarPlugin(Plugin):
pass
# Registered: BarPlugin
print(Plugin.registry)
# {'FooPlugin': ..., 'BarPlugin': ...}
取代了過去要寫 metaclass 才能做的「子類註冊」。Python 3.6+ 的現代慣用法。
__set_name__:descriptor 自動取得名稱
如前面 LazyProperty / Typed 範例:
class MyDescriptor:
def __set_name__(self, owner, name):
self.name = name # 'x', 'y' 等屬性名
class Foo:
x = MyDescriptor() # 自動觸發 __set_name__(Foo, 'x')
y = MyDescriptor() # 自動觸發 __set_name__(Foo, 'y')
Metaclass(一句話帶過)
__init_subclass__ 與 __set_name__ 是 Python 3.6+(PEP 487)為了取代 metaclass 大部分常見用途而設計。除非你在寫 ORM 框架等深層工具,否則不需要碰 metaclass——這是另一個獨立主題,未來可單獨開一篇筆記深入。
異步版本
異步版本就是「dunder method 名稱前加 a」:
| 同步 | 異步 | 對應語法 |
|---|---|---|
__enter__ / __exit__ |
__aenter__ / __aexit__ |
async with |
__iter__ / __next__ |
__aiter__ / __anext__ |
async for |
| — | __await__ |
await obj |
異步 Context Manager
import asyncio
class AsyncTimer:
async def __aenter__(self):
self.start = asyncio.get_event_loop().time()
return self
async def __aexit__(self, exc_type, exc_value, traceback):
elapsed = asyncio.get_event_loop().time() - self.start
print(f"Async elapsed: {elapsed:.3f}s")
async def main():
async with AsyncTimer():
await asyncio.sleep(1)
asyncio.run(main())
異步 Iterator
class AsyncRange:
def __init__(self, n):
self.n = n
self.i = 0
def __aiter__(self):
return self
async def __anext__(self):
if self.i >= self.n:
raise StopAsyncIteration
await asyncio.sleep(0.1)
self.i += 1
return self.i - 1
async def main():
async for x in AsyncRange(3):
print(x)
完整對照速查表
物件生命週期與表示
| 用途 | dunder method |
|---|---|
| 建構物件 | __new__ |
| 初始化 | __init__ |
| 銷毀(不建議用) | __del__ |
| 給人看 | __str__ |
| 給開發者看 | __repr__ |
| f-string 格式 | __format__ |
bytes() 轉換 |
__bytes__ |
bool(obj) |
__bool__ |
比較與雜湊
| 用途 | dunder method |
|---|---|
==, != |
__eq__, __ne__ |
<, <=, >, >= |
__lt__, __le__, __gt__, __ge__ |
hash(obj) |
__hash__ |
算術
| 用途 | dunder method |
|---|---|
| 二元 | __add__ ... 與 __radd__, __iadd__ |
| 一元 | __neg__, __pos__, __abs__, __invert__ |
| 型別轉換 | __int__, __float__, __complex__, __index__ |
容器
| 用途 | dunder method |
|---|---|
| 長度 | __len__ |
| 索引 | __getitem__, __setitem__, __delitem__ |
| 包含 | __contains__ |
| 迭代 | __iter__, __next__, __reversed__ |
Context / Callable / Attribute
| 用途 | dunder method |
|---|---|
| Context manager | __enter__, __exit__ |
| 異步 context | __aenter__, __aexit__ |
| 可呼叫 | __call__ |
| 屬性查詢 | __getattr__, __getattribute__ |
| 屬性設定/刪除 | __setattr__, __delattr__ |
| 列出屬性 | __dir__ |
Descriptor
| 用途 | dunder method |
|---|---|
| 讀 | __get__ |
| 寫 | __set__ |
| 刪 | __delete__ |
| 命名 | __set_name__ |
類別行為
| 用途 | dunder method |
|---|---|
| 子類定義時觸發 | __init_subclass__ |
| Descriptor 命名 | __set_name__ |
序列化與複製
| 用途 | dunder method |
|---|---|
| pickle 支援 | __reduce__, __reduce_ex__ |
| pickle 狀態 | __getstate__, __setstate__ |
| 淺/深拷貝 | __copy__, __deepcopy__ |
最佳實踐
1. 一定要寫 __repr__
# ❌ debug 看到 <__main__.User object at 0x10a3b...>
class User:
def __init__(self, name):
self.name = name
# ✅
class User:
def __init__(self, name):
self.name = name
def __repr__(self):
return f"User(name={self.name!r})"
2. __eq__ 後一定也寫 __hash__
否則類別變 unhashable,無法放進 set / dict key。
3. __exit__ 預設不要抑制例外
# ❌ 永遠回傳 True 會吞掉所有錯誤
def __exit__(self, *args):
self.cleanup()
return True
# ✅ 不回傳(None)→ 例外正常傳出
def __exit__(self, *args):
self.cleanup()
4. 不要用 __del__ 做清理
用 context manager。
5. 簡單情況優先用 dataclass
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
# 自動產生 __init__, __repr__, __eq__
6. 不確定要不要寫 dunder method 時,先別寫
過度使用 dunder method(特別是 __getattr__、__getattribute__)會讓程式碼極難 debug。先用普通方法,真有需要再轉成 dunder。
常見問題
Q1:dunder method 可以直接呼叫嗎?
可以但不建議:
# ❌ 寫法
a.__add__(b)
a.__len__()
# ✅ 寫法
a + b
len(a)
理由:直接呼叫繞過 Python 的 fallback 機制(如 __radd__、__getattr__),可能行為不一致。
Q2:怎麼讓我的物件支援 print()?
實作 __str__(給 print 用)或 __repr__(fallback):
class Foo:
def __str__(self):
return "I am Foo"
print(Foo()) # I am Foo
Q3:__init__ 可以回傳東西嗎?
不行——__init__ 必須回傳 None,否則 raise TypeError。
要回傳特定物件的場景,請改寫 __new__。
Q4:__str__ 與 __repr__ 都可以省略嗎?
可以,但強烈建議至少寫 __repr__——少了它 debug 痛苦。__str__ 沒寫的話會 fallback 到 __repr__。
Q5:什麼時候用類別寫 context manager、什麼時候用 @contextmanager?
| 情境 | 推薦 |
|---|---|
| 只需要 setup + cleanup,邏輯簡單 | @contextmanager |
| 需要狀態(多個屬性、方法) | 類別 |
需要可重複使用(多次 with obj:) |
類別 |
| 一次性 wrap 既有函式 | @contextmanager |
Q6:dunder method 會被繼承嗎?
會,與一般方法繼承規則相同。但有些 dunder(如 __init_subclass__)在繼承時行為特殊,要查官方文件。
Q7:可以動態新增 dunder method 嗎?
class Foo:
pass
Foo.__add__ = lambda self, other: "added"
Foo() + Foo() # ❌ TypeError
不行——dunder method 必須定義在類別上,運行時新增到實例不會被內建語法觸發。這是 Python 為了效能做的特殊查詢規則(C 層級的優化)。
總結
核心要點
- Dunder method 是 Python 物件協定的具體實作——所有語法糖背後都是它
- 理解協定哲學:實作
__iter__就能被for,不必繼承 interface - Context Manager 是清理資源的標準——
__enter__/__exit__取代__del__ - Descriptor 是
@property的底層——理解它就能寫出 ORM 級的魔法 __call__把物件變函式——保有狀態的可呼叫物件__init_subclass__取代了 metaclass 90% 的常見用途
學習路徑建議
入門:__init__、__str__、__repr__、__eq__、__len__
↓
中級:__getitem__、__iter__、__contains__、__call__
↓
進階:__enter__/__exit__、@contextmanager、ExitStack
↓
專家:Descriptor (__get__/__set__)、__set_name__、__init_subclass__
↓
深層:__new__、metaclass、__class_getitem__、attribute lookup 順序
一句話速記表
| Dunder | 一句話 |
|---|---|
__init__ |
初始化(不是建構) |
__new__ |
建構(回傳新實例) |
__repr__ |
debug 看到的字串(必寫) |
__str__ |
使用者看到的字串 |
__eq__ |
== 行為(寫了要連 __hash__) |
__hash__ |
放進 set/dict 的鍵 |
__len__ |
len(obj) |
__getitem__ |
obj[i] |
__iter__ |
for x in obj: |
__contains__ |
x in obj |
__enter__/__exit__ |
with obj: 的進入與離開 |
__call__ |
obj() 像函式呼叫 |
__add__ |
obj + other |
__getattr__ |
找不到屬性時的 fallback |
__get__/__set__ |
descriptor(@property 底層) |
__init_subclass__ |
子類定義時觸發 |
延伸閱讀
- Python Data Model 官方文件
- Fluent Python by Luciano Ramalho(Chapter 1: Pythonic Object Model)
- PEP 487 – Simpler customisation of class creation(
__init_subclass__、__set_name__)
建立日期:2026-04-30 最後更新:2026-04-30