如何为接收被装饰函数参数子集的装饰器做类型注解?
解决带参数子集校验的装饰器类型注解问题
核心思路
要实现支持参数子集校验的装饰器,需利用typing.ParamSpec和typing.Concatenate关联被装饰函数的完整参数与校验函数的参数子集,结合Callable和TypeVar约束返回类型,同时规避ParamSpec不支持绑定的特性。
可通过mypy检查的实现代码
from typing import Callable, ParamSpec, TypeVar, Concatenate # 定义被装饰函数的参数规范和返回值类型 P = ParamSpec("P") R = TypeVar("R") # 定义校验函数的参数子集规范 CheckP = ParamSpec("CheckP") def validate(*checks: Callable[CheckP, None]) -> Callable[[Callable[Concatenate[CheckP, P], R]], Callable[Concatenate[CheckP, P], R]]: def decorator(func: Callable[Concatenate[CheckP, P], R]) -> Callable[Concatenate[CheckP, P], R]: def wrapper(*args: CheckP.args, **kwargs: CheckP.kwargs, /, *extra_args: P.args, **extra_kwargs: P.kwargs) -> R: # 遍历执行所有校验函数,传入对应的参数子集 for check in checks: # 提取校验函数所需的关键字参数 filtered_kwargs = {k: v for k, v in kwargs.items() if k in check.__code__.co_varnames} check(*args, **filtered_kwargs) # 调用原函数,传递所有参数 return func(*args, *extra_args, **kwargs, **extra_kwargs) return wrapper return decorator # 示例校验函数:仅校验age参数 def check_age(age: int) -> None: if age <= 18: raise ValueError("Age must be greater than 18") # 被装饰函数:接收name和age两个参数 @validate(check_age) def greet(name: str, age: int) -> str: return f"Hello {name}, you are {age} years old" # 测试用例 greet("Alice", 20) # 正常执行 # greet("Bob", 17) # 触发校验失败,抛出ValueError
关键说明
- ParamSpec错误修复:你遇到的"Only the first argument to ParamSpec has defined semantics"错误,是因为尝试给ParamSpec添加
bound参数导致的——ParamSpec设计上仅接受名称作为唯一参数,不支持绑定操作。我们通过分别定义CheckP(校验参数子集)和P(剩余参数),再用Concatenate组合,明确参数结构关系。 - 参数子集提取:通过
check.__code__.co_varnames获取校验函数的参数名,从被装饰函数的输入中过滤出对应参数,确保校验函数只拿到它需要的参数子集。 - 参数边界区分:使用
/分隔校验参数和剩余参数,让mypy能正确识别参数子集的边界,避免类型推断混乱。
内容的提问来源于stack exchange,提问作者Fredrik Nilsson
相关产品推荐
相关产品推荐

