📅 2026-05-26 👤 陈强 🏷️ 装饰器 · 元编程 · 闭包

Python 装饰器:元编程与闭包机制深度实践

深入解析 Python 函数对象模型、闭包作用域链、描述符协议、类装饰器执行顺序,以及 dataclass 背后的实现机制,掌握元编程的核心原理。

1. 函数对象模型

1.1 函数是一等公民

Python 中函数是对象,拥有 __code____globals____closure__ 等属性。这使得函数可以被赋值给变量、作为参数传递、从函数返回——这是装饰器能够工作的基础。

函数对象属性
def greet(name: str) -> str:
    return f"Hello, {name}!"

# 函数是对象,拥有以下属性
print(greet.__name__)       # greet
print(greet.__code__)      # code 对象
print(greet.__code__.co_varnames)  # ('name',)
print(greet.__code__.co_freevars)  # 自由变量名
print(greet.__globals__)   # 定义所在模块的 globals
print(greet.__closure__)    # closure 元组或 None
print(greet.__annotations__) # {'name': str, 'return': str}
print(greet.__call__)       # 内置方法

1.2 函数 vs 方法 vs 类

在 Python 中,def 定义的函数、lambda 表达式、以及类方法都是可调用对象,但它们的创建机制不同。理解这一区别是掌握元编程的关键。

函数创建机制
# 普通函数 - 在模块作用域定义
def func(): pass

# 闭包函数 - 捕获外部作用域变量
def outer(x):
    def inner(y):
        return x + y  # x 是自由变量,捕获自 outer
    return inner

# 类方法 - 通过 descriptor 协议转换
class MyClass:
    def method(self): pass

# bound method - 实例获取方法时自动绑定
obj = MyClass()
m = obj.method
print(m.__self__) # obj - 绑定到的实例

1.3 Callable 协议

任何对象如果定义了 __call__ 方法,就是可调用的。装饰器本质上就是把被装饰函数替换为 __call__ 执行特定逻辑的可调用对象。

自定义可调用对象
class Counter:
    def __init__(self):
        self.count = 0

    def __call__(self, func):
        # 让 Counter 实例本身成为装饰器
        def wrapper(*args, **kwargs):
            self.count += 1
            print(f"Call #{self.count}")
            return func(*args, **kwargs)
        return wrapper

counter = Counter()

@counter
def add(a, b):
    return a + b

add(1, 2)  # Call #1
add(3, 4)  # Call #2
print(counter.count)  # 2

2. 闭包作用域链

2.1 LEGB 规则

Python 变量查找遵循 LEGB 规则:Local(局部)→ Enclosing(闭包)→ Global(全局)→ Builtins(内置)。闭包函数能访问外层函数的局部变量,正是通过 Enclosing 作用域实现的。

LEGB 作用域链
# LEGB 示例
x = "global"

def outer():
    x = "enclosing"

    def inner():
        x = "local"
        print(x)  # 1. 找到 local: "local"

    inner()
    print(x)      # 2. 找到 enclosing: "enclosing"

outer()
print(x)          # 3. 找到 global: "global"

# 使用 nonlocal 在闭包中修改外层变量
def outer():
    x = "enclosing"

    def inner():
        nonlocal x
        x = "modified"

    inner()
    print(x)  # "modified"

2.2 Cell 对象机制

闭包捕获的变量存储在 Cell 对象中。函数的 __closure__ 属性是一个 Cell 元组,每个 Cell 包含实际值的引用。

Cell 对象详解
def outer(x):
    def inner(y):
        return x + y
    return inner

闭包 = outer(10)
print(闭包.__closure__)  # (,)
print(闭包.__closure__[0].cell_contents)  # 10 - 捕获的值

# 注意:如果变量不是来自外层作用域,__closure__ 为 None
def standalone():
    def inner():
        return 42
    return inner

no_closure = standalone()
print(no_closure.__closure__)  # None

2.3 闭包陷阱

闭包最经典的陷阱是在循环中捕获变量——所有闭包函数共享同一个变量引用,而非各自的副本。

闭包变量捕获陷阱
# 陷阱:所有函数捕获同一个变量
funcs = []
for i in range(3):
    funcs.append(lambda: i)

for f in funcs:
    print(f(), end=" ")  # 2 2 2 而非 0 1 2

# 解决:使用默认参数捕获当前值
funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)  # i=i 捕获当前 i 的值

for f in funcs:
    print(f(), end=" ")  # 0 1 2

# 或使用 functools.partial
import functools
funcs = []
for i in range(3):
    funcs.append(functools.partial(lambda x: x, i))
💡 关键洞察

闭包捕获的是变量引用而非值。这意味着如果你在外层函数中修改了捕获的变量,闭包函数下次调用时会看到新值。使用 nonlocal 声明可以允许闭包修改外层变量,但这通常不是好的设计实践。

3. 描述符协议

3.1 描述符定义

描述符是实现了 __get____set____delete__ 三个方法中任意一个的对象。当访问对象的某个属性时,如果该属性是描述符,Python 会自动调用对应的描述符方法。

描述符协议
class Descriptor:
    def __get__(self, obj, objtype=None):
        # 当访问属性时调用
        return f"Getting from {obj}"

    def __set__(self, obj, value):
        # 当设置属性时调用
        print(f"Setting {value} on {obj}")

    def __delete__(self, obj):
        # 当删除属性时调用
        print(f"Deleting on {obj}")

class MyClass:
    attr = Descriptor()

obj = MyClass()
obj.attr      # 调用 __get__
obj.attr = 42  # 调用 __set__
del obj.attr  # 调用 __delete__

3.2 Python 内置描述符

Python 的属性查找机制本身就是基于描述符实现的。propertyclassmethodstaticmethod 都是描述符。

Python 内置描述符
# property - 数据描述符(同时有 __get__ 和 __set__)
class MyClass:
    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, v):
        self._value = v

# classmethod - 非数据描述符(只有 __get__)
class MyClass:
    @classmethod
    def factory(cls, *args):
        return cls(*args)

# staticmethod - 非数据描述符(只有 __get__)
class MyClass:
    @staticmethod
    def  utility():
        return "utility"

# 描述符优先级: 数据描述符 > 实例属性 > 非数据描述符 > __getattr__

3.3 描述符实现原理

描述符协议的核心在于 object.__getattribute__ 的实现逻辑。当访问 obj.attr 时,Python 会遍历 MRO 查找描述符并调用。

描述符查找流程
# object.__getattribute__ 简化逻辑
def __getattribute__(self, name):
    # 1. 查找 MRO 中所有基类
    for cls in self.__class__.__mro__:
        # 2. 如果类中有数据描述符(__get__ + __set__)
        if name in cls.__dict__ and hasattr(cls.__dict__[name], '__get__'):
            descriptor = cls.__dict__[name]
            # 调用描述符的 __get__
            return descriptor.__get__(self, cls.__dict__[name])

        # 3. 查找实例 __dict__
        if name in self.__dict__:
            return self.__dict__[name]

        # 4. 如果是非数据描述符(只有 __get__)
        if name in cls.__dict__:
            attr = cls.__dict__[name]
            if hasattr(attr, '__get__'):
                return attr.__get__(self, type(self))

    # 5. 最后尝试 __getattr__
    raise AttributeError(name)

4. 类装饰器顺序

4.1 装饰器求值时机

类装饰器在类定义体执行完毕后立即求值,从上到下依次应用。这意味着下层装饰器先于上层装饰器执行。

装饰器应用顺序
def decorator_a(cls):
    print("A")
    return cls

def decorator_b(cls):
    print("B")
    return cls

@decorator_a
@decorator_b
class MyClass:
    pass

# 输出顺序: B A
# 因为 decorator_b 先应用到 MyClass,
# 然后 decorator_a 应用到 decorator_b 的返回值

4.2 带参数的装饰器

带参数的装饰器需要额外一层函数来接收参数。通常使用 functools.wraps 保持被装饰对象的元信息。

带参数的类装饰器
import functools

def with_metaclass(meta, **kwargs):
    def decorator(cls):
        @functools.wraps(cls)
        class Wrapper(meta(cls)):
            pass
        return Wrapper
    return decorator

# 使用示例
@with_metaclass(type, debug=True)
class DebugClass:
    pass

# functools.wraps 保持 __name__, __doc__ 等
print(DebugClass.__name__)  # DebugClass (而非 Wrapper)

4.3 装饰器链与元类链

当装饰器与元类同时存在时,执行顺序为:类定义体 → 装饰器(从下到上)→ 元类。元类的 __new__ 在类创建时被调用。

装饰器与元类执行顺序
# 定义两个装饰器
def add_method(cls):
    cls.new_method = lambda self: "added"
    return cls

def add_class_attribute(cls):
    cls.attr = "class level"
    return cls

# 定义元类
class MyMeta(type):
    def __new__(mcs, name, bases, namespace):
        print(f"Meta: creating {name}")
        return super().__new__(mcs, name, bases, namespace)

# 应用顺序:
# 1. 类定义体执行
# 2. add_class_attribute 装饰器应用到 MyClass
# 3. add_method 装饰器应用到 MyClass
# 4. MyMeta.__new__ 被调用,创建类对象
@add_method
@add_class_attribute
class MyClass(metaclass=MyMeta):
    pass

# Meta: creating MyClass (最后由元类创建)

5. dataclass 原理

5.1 dataclass 实现机制

@dataclass 是 Python 3.7 引入的类装饰器,它自动生成 __init____repr____eq____lt__ 等方法,极大简化了数据类的定义。

dataclass 基础用法
from dataclasses import dataclass, field
from typing import List

@dataclass
class Point:
    x: float
    y: float

@dataclass
class User:
    name: str
    email: str
    tags: List[str] = field(default_factory=list)
    active: bool = True

# dataclass 自动生成:
# - __init__(self, name, email, tags, active)
# - __repr__(self) -> "User(name=..., email=..., tags=..., active=...)"
# - __eq__(self, other) -> 基于所有字段比较
# - __lt__, __le__, __gt__, __ge__ (如果 order=True)
# - __hash__ (如果 frozen=True)

5.2 dataclass 字段机制

field 函数提供细粒度控制:default_factory 用于可变默认值,compare 控制是否参与比较,hash 控制是否可哈希。

field 详细配置
from dataclasses import dataclass, field

@dataclass
class Config:
    name: str
    items: List[str] = field(
        default_factory=list,  # 可变默认值必须用 factory
        compare=True,          # 是否参与 __eq__ 比较
        hash=True,               # 是否参与 __hash__
        init=True,              # 是否生成 __init__ 参数
        repr=True,              # 是否在 __repr__ 中显示
        metadata=None           # 任意元数据字典
    )

# metadata 访问
print(Config.__dataclass_fields__["items"].metadata)
# Field(name='items', type=List[str], default_factory=list, ...)

5.3 dataclass 与 slots

Python 3.10 引入的 slots=True 选项使 dataclass 使用 __slots__ 存储属性,内存占用减少约 40%。

dataclass + slots
from dataclasses import dataclass
import sys

@dataclass
class RegularUser:
    name: str
    email: str

@dataclass(slots=True)
class SlottedUser:
    name: str
    email: str

# 内存对比
r = RegularUser("Alice", "alice@example.com")
s = SlottedUser("Alice", "alice@example.com")

print(sys.getsizeof(r.__dict__))  # ~104 bytes
print(sys.getsizeof(s.__slots__)) # ~56 bytes

# 注意:slots=True 时不能动态添加属性
# s.new_attr = "value"  # AttributeError
⚠️ dataclass 注意事项

dataclass 默认会生成基于所有字段的 __eq__,这意味着字段顺序改变会影响对象相等性判断。对于需要稳定哈希或跨版本兼容的类,应显式设置 frozen=True 或使用 field(compare=False)