類別裝飾器是比 Metaclass 更簡單、更實用的元編程工具。大多數情況下,類別裝飾器就能滿足需求。

先備知識

本章目標

學完本章後,你將能夠:

  1. 編寫類別裝飾器
  2. 理解 @dataclass 的實現原理
  3. 使用 type() 動態建立類別
  4. 選擇合適的元編程工具

【原理層】類別裝飾器

基本結構

類別裝飾器是一個接收類別、返回類別的函式:

 1def my_decorator(cls):
 2    # 修改或增強 cls
 3    return cls
 4
 5@my_decorator
 6class MyClass:
 7    pass
 8
 9# 等價於
10class MyClass:
11    pass
12MyClass = my_decorator(MyClass)

與函式裝飾器的差異

 1# 函式裝飾器通常返回包裝函式
 2def func_decorator(func):
 3    def wrapper(*args, **kwargs):
 4        print("Before")
 5        result = func(*args, **kwargs)
 6        print("After")
 7        return result
 8    return wrapper
 9
10# 類別裝飾器通常直接修改類別
11def class_decorator(cls):
12    cls.new_attribute = "added"
13    return cls

執行時機

 1def decorator(cls):
 2    print(f"裝飾 {cls.__name__}")
 3    return cls
 4
 5@decorator
 6class MyClass:
 7    print("定義類別主體")
 8
 9# 輸出:
10# 定義類別主體
11# 裝飾 MyClass

類別裝飾器在類別定義完成後立即執行。


【設計層】帶參數的裝飾器

裝飾器工廠

 1def repeat_method(times):
 2    """讓指定方法重複執行 times 次"""
 3    def decorator(cls):
 4        original_init = cls.__init__
 5
 6        def new_init(self, *args, **kwargs):
 7            for _ in range(times):
 8                original_init(self, *args, **kwargs)
 9
10        cls.__init__ = new_init
11        return cls
12
13    return decorator
14
15@repeat_method(3)
16class Counter:
17    count = 0
18
19    def __init__(self):
20        Counter.count += 1
21
22c = Counter()
23print(Counter.count)  # 3

實用範例:@singleton

 1def singleton(cls):
 2    """單例模式裝飾器"""
 3    instances = {}
 4
 5    def get_instance(*args, **kwargs):
 6        if cls not in instances:
 7            instances[cls] = cls(*args, **kwargs)
 8        return instances[cls]
 9
10    return get_instance
11
12@singleton
13class Database:
14    def __init__(self, host):
15        self.host = host
16        print(f"連接到 {host}")
17
18db1 = Database("localhost")  # 連接到 localhost
19db2 = Database("remote")     # 不會再次連接
20print(db1 is db2)  # True

注意:這個實現返回的是函式而不是類別,所以 isinstance(db1, Database) 會失敗。更好的實現:

 1def singleton(cls):
 2    """保持類別特性的單例裝飾器"""
 3    _instance = None
 4
 5    class SingletonWrapper(cls):
 6        def __new__(wrapper_cls, *args, **kwargs):
 7            nonlocal _instance
 8            if _instance is None:
 9                _instance = super().__new__(wrapper_cls)
10            return _instance
11
12    SingletonWrapper.__name__ = cls.__name__
13    SingletonWrapper.__qualname__ = cls.__qualname__
14    return SingletonWrapper

【實作層】實用類別裝飾器

@auto_repr

 1def auto_repr(cls):
 2    """自動生成 __repr__ 方法"""
 3    def __repr__(self):
 4        attrs = ', '.join(
 5            f"{k}={v!r}"
 6            for k, v in self.__dict__.items()
 7            if not k.startswith('_')
 8        )
 9        return f"{cls.__name__}({attrs})"
10
11    cls.__repr__ = __repr__
12    return cls
13
14@auto_repr
15class Person:
16    def __init__(self, name, age):
17        self.name = name
18        self.age = age
19
20p = Person("Alice", 30)
21print(p)  # Person(name='Alice', age=30)

@dataclass 的簡化實現

 1def dataclass(cls):
 2    """簡化版 dataclass"""
 3    # 收集類別屬性中的型別註解
 4    annotations = getattr(cls, '__annotations__', {})
 5
 6    # 生成 __init__
 7    def __init__(self, **kwargs):
 8        for name in annotations:
 9            setattr(self, name, kwargs.get(name))
10
11    # 生成 __repr__
12    def __repr__(self):
13        attrs = ', '.join(
14            f"{name}={getattr(self, name)!r}"
15            for name in annotations
16        )
17        return f"{cls.__name__}({attrs})"
18
19    # 生成 __eq__
20    def __eq__(self, other):
21        if not isinstance(other, cls):
22            return NotImplemented
23        return all(
24            getattr(self, name) == getattr(other, name)
25            for name in annotations
26        )
27
28    cls.__init__ = __init__
29    cls.__repr__ = __repr__
30    cls.__eq__ = __eq__
31
32    return cls
33
34@dataclass
35class Point:
36    x: int
37    y: int
38
39p1 = Point(x=1, y=2)
40p2 = Point(x=1, y=2)
41print(p1)         # Point(x=1, y=2)
42print(p1 == p2)   # True

@timer(方法計時)

 1import time
 2from functools import wraps
 3
 4def timer(cls):
 5    """為所有公開方法添加計時"""
 6    for name, method in list(cls.__dict__.items()):
 7        if callable(method) and not name.startswith('_'):
 8            @wraps(method)
 9            def timed_method(self, *args, _method=method, _name=name, **kwargs):
10                start = time.perf_counter()
11                result = _method(self, *args, **kwargs)
12                elapsed = time.perf_counter() - start
13                print(f"{_name}: {elapsed:.4f}s")
14                return result
15
16            setattr(cls, name, timed_method)
17
18    return cls
19
20@timer
21class Calculator:
22    def slow_add(self, a, b):
23        time.sleep(0.1)
24        return a + b
25
26calc = Calculator()
27calc.slow_add(1, 2)  # slow_add: 0.1001s

【設計層】動態建立類別

使用 type()

 1# 基本用法
 2def greet(self):
 3    return f"Hello, {self.name}"
 4
 5Person = type('Person', (), {
 6    'species': 'Human',
 7    'greet': greet,
 8})
 9
10p = Person()
11p.name = "Alice"
12print(p.greet())  # Hello, Alice

動態繼承

 1def create_model(name, fields):
 2    """動態建立模型類別"""
 3
 4    def __init__(self, **kwargs):
 5        for field in fields:
 6            setattr(self, field, kwargs.get(field))
 7
 8    def __repr__(self):
 9        attrs = ', '.join(f"{f}={getattr(self, f)!r}" for f in fields)
10        return f"{name}({attrs})"
11
12    return type(name, (), {
13        '__init__': __init__,
14        '__repr__': __repr__,
15        '_fields': fields,
16    })
17
18User = create_model('User', ['id', 'name', 'email'])
19Product = create_model('Product', ['id', 'name', 'price'])
20
21u = User(id=1, name="Alice", email="alice@example.com")
22print(u)  # User(id=1, name='Alice', email='alice@example.com')

條件繼承

 1def create_handler(use_async=False):
 2    """根據條件選擇基類"""
 3
 4    if use_async:
 5        import asyncio
 6
 7        class AsyncBase:
 8            async def handle(self, data):
 9                await asyncio.sleep(0.1)
10                return f"Async: {data}"
11
12        base = AsyncBase
13    else:
14        class SyncBase:
15            def handle(self, data):
16                return f"Sync: {data}"
17
18        base = SyncBase
19
20    return type('Handler', (base,), {})
21
22SyncHandler = create_handler(use_async=False)
23AsyncHandler = create_handler(use_async=True)

【選擇指南】

Metaclass vs 類別裝飾器 vs init_subclass

特性類別裝飾器init_subclassMetaclass
學習曲線
影響子類別
可堆疊是(需 super)困難
控制程度類別建立後子類別定義時類別建立中
適用場景添加/修改方法註冊/驗證深度自訂

決策流程

 1需要修改類別的行為?
 2 3├── 只在單一類別上?
 4│   └── 類別裝飾器
 5 6├── 需要自動影響所有子類別?
 7│   ├── 只是註冊或簡單驗證?
 8│   │   └── __init_subclass__
 9│   │
10│   └── 需要修改類別建立過程?
11│       └── Metaclass
1213└── 需要完全動態建立類別?
14    └── type()

【實戰】組合使用

 1from functools import wraps
 2
 3def validate_types(cls):
 4    """型別驗證裝飾器"""
 5    original_init = cls.__init__
 6    annotations = getattr(cls, '__annotations__', {})
 7
 8    @wraps(original_init)
 9    def validated_init(self, *args, **kwargs):
10        # 將位置參數轉換為關鍵字參數
11        import inspect
12        sig = inspect.signature(original_init)
13        params = list(sig.parameters.keys())[1:]  # 跳過 self
14
15        for i, arg in enumerate(args):
16            if i < len(params):
17                kwargs[params[i]] = arg
18
19        # 驗證型別
20        for name, expected_type in annotations.items():
21            if name in kwargs:
22                value = kwargs[name]
23                if not isinstance(value, expected_type):
24                    raise TypeError(
25                        f"{name} 應為 {expected_type.__name__},"
26                        f"但得到 {type(value).__name__}"
27                    )
28
29        original_init(self, **kwargs)
30
31    cls.__init__ = validated_init
32    return cls
33
34@validate_types
35class User:
36    name: str
37    age: int
38
39    def __init__(self, name, age):
40        self.name = name
41        self.age = age
42
43User("Alice", 30)  # OK
44# User("Bob", "thirty")  # TypeError!

思考題

  1. 類別裝飾器和 Metaclass 可以同時使用嗎?執行順序是什麼?
  2. @dataclass 實際上使用了哪些技術?
  3. 動態建立的類別和靜態定義的類別有什麼差異?

實作練習

  1. 實作一個 @frozen 裝飾器,讓類別的實例不可變
  2. 實作一個 @trace 裝飾器,追蹤所有方法呼叫
  3. 使用 type() 建立一個簡單的 Enum 類別

延伸閱讀


上一章:Metaclass 設計與應用 下一章:反射與 inspect 模組