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()
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 示例
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 包含实际值的引用。
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
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 的属性查找机制本身就是基于描述符实现的。property、classmethod、staticmethod 都是描述符。
# property - 数据描述符(同时有 __get__ 和 __set__)
class MyClass:
def value(self):
return self._value
def value(self, v):
self._value = v
# classmethod - 非数据描述符(只有 __get__)
class MyClass:
def factory(cls, *args):
return cls(*args)
# staticmethod - 非数据描述符(只有 __get__)
class MyClass:
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
class MyClass:
pass
# 输出顺序: B A
# 因为 decorator_b 先应用到 MyClass,
# 然后 decorator_a 应用到 decorator_b 的返回值
4.2 带参数的装饰器
带参数的装饰器需要额外一层函数来接收参数。通常使用 functools.wraps 保持被装饰对象的元信息。
functools
def with_metaclass(meta, **kwargs):
def decorator(cls):
(cls)
class Wrapper(meta(cls)):
pass
return Wrapper
return decorator
# 使用示例
(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__ 被调用,创建类对象
class MyClass(metaclass=MyMeta):
pass
# Meta: creating MyClass (最后由元类创建)
5. dataclass 原理
5.1 dataclass 实现机制
@dataclass 是 Python 3.7 引入的类装饰器,它自动生成 __init__、__repr__、__eq__、__lt__ 等方法,极大简化了数据类的定义。
dataclasses dataclass, field
typing List
class Point:
x: float
y: float
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 控制是否可哈希。
dataclasses dataclass, field
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%。
dataclasses dataclass
sys
class RegularUser:
name: str
email: str
(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 默认会生成基于所有字段的 __eq__,这意味着字段顺序改变会影响对象相等性判断。对于需要稳定哈希或跨版本兼容的类,应显式设置 frozen=True 或使用 field(compare=False)。