装饰器类型注解疑难:如何正确注解替换函数参数的装饰器并确保functools.wraps正常工作?
看起来你遇到的核心问题是装饰器的类型注解参数顺序、类型匹配不对,导致mypy报了一堆参数不兼容的错误。你的Injector装饰器逻辑本身是没问题的——自动注入deps参数、禁止用户手动传这个参数,但类型注解的方向搞反了,才触发了这些检查错误。
先说说当前代码的错误原因
你原来的__call__方法用了Concatenate[dict[str, Any], P],这在类型系统里表示:目标函数的第一个参数必须是dict(也就是deps),后面才是P定义的其他参数。但你的foo函数是先传baz: str,再传deps,参数顺序完全反过来了,而且deps在实际调用中是作为关键字参数传入的,类型注解没体现这一点,自然会被mypy揪出来。
另外,functools.wraps本身是正常工作的,它会正确复制原函数的元数据(函数名、文档、参数信息等),问题完全出在类型注解上,和wraps无关。
修正后的完整代码
from typing import ParamSpec, TypeVar, Callable, Any, Protocol import functools P = ParamSpec("P") T = TypeVar("T") # 可选:定义协议,强制目标函数必须接受deps关键字参数 class RequiresDepsParam(Protocol[P, T]): def __call__(self, *args: P.args, deps: dict[str, Any], **kwargs: P.kwargs) -> T: ... class Injector: _registered_dependencies: dict[str, Any] = {} def __init__(self, *dependency: str): self.dependency = dependency # 修正后的类型注解: # 目标函数需要接受P的参数 + deps关键字参数,装饰后只返回接受P参数的函数 def __call__(self, func: RequiresDepsParam[P, T]) -> Callable[P, T]: @functools.wraps(func) def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: if "deps" in kwargs: raise ValueError("Argument 'deps' is already provided and will be overwritten.") try: deps = { i: self.__class__._registered_dependencies[i] for i in self.dependency } except KeyError as e: raise KeyError(f"Dependency {e.args[0]} is not registered") # 始终以关键字参数传入deps,和协议定义一致 return func(*args, **kwargs, deps=deps) return wrapper @classmethod def register(cls, id: str, dependency: Any) -> None: if not isinstance(id, str): raise TypeError("Dependency ID must be a string") if id in cls._registered_dependencies: raise ValueError(f"Dependency '{id}' is already registered") cls._registered_dependencies[id] = dependency # 目标函数把deps设为关键字-only参数,和协议定义匹配 @Injector('test') def foo(baz: str, *, deps: dict[str, Any]) -> None: print(f'{baz=}, {deps=}') # baz='bar', deps={'test': 423} if __name__ == '__main__': Injector.register('test', 423) foo('bar') # mypy完全不会报错了
关键修改点说明
用协议强制目标函数的参数要求
我定义了RequiresDepsParam协议,明确要求传入装饰器的函数必须接受deps关键字参数。这样mypy会自动检查所有被@Injector装饰的函数,如果没定义deps参数,就会直接报错,从源头保证类型安全。修正装饰器的类型转换逻辑
现在__call__的签名是func: RequiresDepsParam[P, T] -> Callable[P, T],表示:- 输入的函数
func需要接受P的参数 +deps关键字参数 - 输出的装饰后函数只需要接受
P的参数(也就是自动帮你去掉了deps参数的要求)
- 输入的函数
把目标函数的deps设为关键字-only参数
在foo函数里用*, deps: dict[str, Any]的写法,把deps变成只能用关键字传入的参数。这和装饰器中deps=deps的传递方式完全一致,避免了位置参数顺序的混淆,也让mypy的检查更精准。
关于functools.wraps的说明
你完全不用担心functools.wraps的工作问题,它会正确复制原函数的所有元数据——包括函数名、文档字符串、参数列表等。装饰后的foo函数,无论是运行时的行为还是类型系统的认知,都会符合预期。
备注:内容来源于stack exchange,提问作者AsfhtgkDavid

