装饰器、描述符、property 与属性访问
本节目标
查询 Python 装饰器、描述符协议、property 及属性访问优先级。
装饰器在定义时替换函数或类绑定;描述符决定类属性经实例访问时的行为;property 是托管属性的常用描述符。本文以 Python 3.14 描述符指南、函数定义参考和 functools.wraps 文档为边界,并接续类、继承、组合与 dataclass与Python 数据模型与特殊方法。
装饰器语法与函数替换
@decorate 在函数对象创建后、名字绑定前应用,@outer 写在 @inner 外层时,结果是 target = outer(inner(target))。因此替换发生在定义语句执行时,而不是每次调用时。
@decorate
def target() -> str:
return "value"
绑定时,target 被装饰器返回的可调用对象替代;应先确认返回对象仍符合调用方所需的接口。
包装器、闭包与 functools.wraps
包装器通常闭包捕获原函数,并在调用时转发参数。functools.wraps 把原函数的常用元数据复制到包装器,并设置 __wrapped__,让内省或工具能沿包装链找到原函数。
from functools import wraps
def logged(function):
@wraps(function)
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
绑定时名字指向 wrapper;调用时 wrapper 再调用闭包中的 function,所以计数、日志等状态应明确放在这一层。
参数化装饰器与装饰器工厂
@repeat(2) 先调用工厂,再用工厂返回的装饰器替换目标,即 target = repeat(2)(target)。相较无参数装饰器,多出“工厂调用”和“返回装饰器接收目标”两层。
def repeat(times: int):
def decorate(function):
return function
return decorate
工厂在定义时接收配置,装饰器随后在定义时接收函数;真正的业务函数仍在之后的调用时执行。
类装饰器与可调用对象
装饰器也可以接收类并返回替换后的类或其他可调用对象。实现 __call__ 的实例同样可作装饰器;实例状态会在定义时参与保存,并在后续调用时生效。
class Prefix:
def __call__(self, function):
return function
@Prefix()
def label() -> str:
return "Python"
定义时构造 Prefix() 并调用它;返回值才绑定为 label,所以类装饰器不能假定名字仍指向原函数。
描述符协议:__get__、__set__、__delete__
类字典中的对象只要定义 __get__、__set__ 或 __delete__ 之一,就是描述符。实例读取可触发 __get__(instance, owner);赋值与删除分别触发 __set__ 和 __delete__。代表脚本把实际值存到实例的私有名称中。
from functools import wraps
def count_calls(function):
calls = 0
@wraps(function)
def wrapper(*args, **kwargs):
nonlocal calls
calls += 1
wrapper.calls = calls
return function(*args, **kwargs)
wrapper.calls = 0
return wrapper
class NonNegative:
def __set_name__(self, owner, name: str) -> None:
self.public_name = name
self.private_name = f"_{name}"
def __get__(self, instance, owner=None):
if instance is None:
return self
return getattr(instance, self.private_name)
def __set__(self, instance, value: int) -> None:
if value < 0:
raise ValueError("non-negative")
setattr(instance, self.private_name, value)
class Box:
width = NonNegative()
height = NonNegative()
def __init__(self, width: int, height: int) -> None:
self.width = width
self.height = height
@count_calls
def area(self) -> int:
return self.width * self.height
@property
def label(self) -> str:
return f"box:{self.width}x{self.height}"
box = Box(3, 4)
area = box.area()
try:
box.width = -1
except ValueError as error:
validation = str(error)
else:
validation = "missing"
box.__dict__["width"] = 99
lookup = "data-descriptor" if box.width == 3 else "instance"
print(f"decorated={box.area.__name__}")
print(f"calls={box.area.calls}")
print(f"descriptor-name={Box.width.public_name}")
print(f"area={area}")
print(f"validation={validation}")
print(f"property={box.label}")
print(f"lookup={lookup}")
decorated=area
calls=1
descriptor-name=width
area=12
validation=non-negative
property=box:3x4
lookup=data-descriptor
读取 box.width 时,类属性 NonNegative 的 __get__ 先被调用;写入 box.width 时,__set__ 先验证再存入 _width。
__set_name__ 与存储名称
类创建完成时,若类字典中的描述符定义了 __set_name__,Python 自动以所属类和属性名调用它。这样一个描述符类可为 width 与 height 分别计算 _width 和 _height,无需硬编码字段名。
class Field:
def __set_name__(self, owner, name: str) -> None:
self.private_name = f"_{name}"
这是类创建时的一次通知;运行后再把描述符赋给类属性时,需要显式调用 __set_name__ 才会获得同样的名称初始化。
数据描述符、非数据描述符与查找优先级
定义 __set__ 或 __delete__ 的描述符是数据描述符;仅定义 __get__ 的是非数据描述符。实例读取的优先级为:数据描述符 → 实例属性 → 非数据描述符 → 普通类属性 → getattr。函数是生成绑定方法的非数据描述符,因此实例字典中的同名属性可以遮蔽它。
class Data:
def __get__(self, instance, owner=None):
return 3
def __set__(self, instance, value) -> None:
pass
脚本向 box.__dict__["width"] 写入 99,但读取仍为 3:width 是数据描述符,查找先于实例字典。
property 与托管属性
property 把读取函数包装为描述符;有 setter 或 deleter 时还能托管赋值或删除。Box.label 在每次读取时调用 getter,根据当时的 width、height 计算标签,而不是在初始化时缓存字符串。
class Box:
@property
def label(self) -> str:
return f"box:{self.width}x{self.height}"
读取 box.label 触发 property 的 getter;未定义 setter 时,对该属性赋值会失败,不会自动把值写入实例字典。
__getattribute__、__getattr__ 与 __setattr__
每次实例属性读取先进入 __getattribute__;只有它以 AttributeError 失败后,点号和 getattr 才会尝试 __getattr__。自定义 __setattr__ 处理写入时,应委托给 object.__setattr__ 以保留通常的描述符赋值规则。
class SafeAccess:
def __getattribute__(self, name: str):
return object.__getattribute__(self, name)
def __setattr__(self, name: str, value) -> None:
object.__setattr__(self, name, value)
错误覆写 __getattribute__ 可能绕过正常描述符机制;除非确实要改变全局读取语义,否则不要把它当作普通属性逻辑的首选入口。
包装器、描述符与属性访问边界
这三类机制作用在不同时点:装饰器在定义时替换绑定,包装器在调用时执行,描述符则在属性访问时参与查找。装饰后的名字仍可调用,并不证明它仍是原函数;wraps 虽提供 __wrapped__,却不会自动修复不兼容的签名或返回值。
def plain(function):
def wrapper(*args, **kwargs):
return function(*args, **kwargs)
return wrapper
实例字典也不能单独决定属性读取结果:数据描述符的优先级更高,property 可在读取时计算值,错误的访问钩子覆盖还可能改变或跳过这些默认规则。排查问题时应按实际查找优先级追踪绑定来源。