Python Dunder Methods 完全指南

Python 魔術方法(__init__、__enter__、__call__ 等)的完整介紹,從協定哲學到 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__ 用於:

  • 不可變型別繼承(如繼承 inttuplestr
  • 單例模式(Singleton)
  • Metaclass 自訂類別建立流程(深層用法)

__del__:解構子(多數情況不該用)

class Resource:
    def __del__(self):
        print("Resource cleaned up")

為什麼不該用

  1. 呼叫時機不保證:CPython 用引用計數即時呼叫,PyPy 等實作不一定
  2. 循環引用會延遲呼叫:被 GC 處理才會跑
  3. 直譯器結束時不一定執行

⚠️ 清理資源請用 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 * arrarr * 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 層級的優化)。


總結

核心要點

  1. Dunder method 是 Python 物件協定的具體實作——所有語法糖背後都是它
  2. 理解協定哲學:實作 __iter__ 就能被 for,不必繼承 interface
  3. Context Manager 是清理資源的標準——__enter__/__exit__ 取代 __del__
  4. Descriptor 是 @property 的底層——理解它就能寫出 ORM 級的魔法
  5. __call__ 把物件變函式——保有狀態的可呼叫物件
  6. __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__ 子類定義時觸發

延伸閱讀


建立日期:2026-04-30 最後更新:2026-04-30

🔗相關文章