如何实现可装饰@property且接收参数的Python字段校验装饰器
问题根源
装饰器逻辑不触发由两个核心问题导致:
- 装饰器堆叠顺序错误:当前写法将
@required_fields放在@property外层,此时传入装饰器的参数f是property描述符对象,不是类中定义的getter方法。最终返回的wrapper是普通函数,会直接替换掉原本的property,实例访问属性时不会触发预设的校验逻辑。 - 第一版装饰器存在逻辑缺陷:校验完成后直接
return f,没有调用被装饰的目标方法,即使顺序正确也无法返回正确的属性计算结果。
解决方案
方案1:调整装饰器堆叠顺序(改动最小)
你写的第二版装饰器核心逻辑已经正确,只需要调整装饰器的应用顺序:将@property放在最外层,@required_fields放在靠近被装饰方法的位置。装饰器执行遵循「从近到远」的规则,会先给原始getter方法加上校验逻辑,再把包装后的方法转成property属性,访问时就会正常触发校验。
修正后的完整可运行代码:
import functools import dataclasses import typing import datetime M = float def required_fields(required_fields): def _required_fields(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): for field in required_fields: if getattr(self, field) is None: raise ValueError(f"Missing {field}") return f(self, *args, **kwargs) return wrapper return _required_fields @dataclasses.dataclass class LoanCalculator: _amount: typing.Optional[M] = None _interest_rate: typing.Optional[M] = None _years: typing.Optional[M] = None _balance: typing.Optional[M] = None _payment_day: typing.Optional[int] = None _start_date: typing.Optional[datetime.date] = None class MissingDataError(Exception): pass # 注意装饰器顺序:@property 在外层,@required_fields 在内层 @property @required_fields(['_interest_rate']) def monthly_interest_rate(self): return self._interest_rate / 12
测试效果:
calc = LoanCalculator() # 未设置_interest_rate时,抛出 ValueError: Missing _interest_rate print(calc.monthly_interest_rate) calc._interest_rate = 0.12 # 正常返回计算结果 0.01 print(calc.monthly_interest_rate)
方案2:编写兼容property的装饰器(无需记忆顺序)
如果不想每次手动调整装饰器顺序,可以修改装饰器逻辑,自动识别传入的对象是否为property实例,提取内部的getter方法做包装后再重新生成property返回。这种写法不管装饰器顺序如何都能正常运行:
def required_fields(required_fields): def _required_fields(f): # 识别被装饰对象是property的情况 if isinstance(f, property): original_getter = f.fget @functools.wraps(original_getter) def wrapper(self, *args, **kwargs): for field in required_fields: if getattr(self, field) is None: raise ValueError(f"Missing {field}") return original_getter(self, *args, **kwargs) # 保留原property的setter、deleter和文档 return property(wrapper, f.fset, f.fdel, f.__doc__) # 普通实例方法的处理逻辑 @functools.wraps(f) def wrapper(self, *args, **kwargs): for field in required_fields: if getattr(self, field) is None: raise ValueError(f"Missing {field}") return f(self, *args, **kwargs) return wrapper return _required_fields
内容的提问来源于stack exchange,提问作者Milano
相关产品推荐
相关产品推荐

