You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

装饰器类型注解疑难:如何正确注解替换函数参数的装饰器并确保functools.wraps正常工作?

装饰器类型注解疑难:如何正确注解替换函数参数的装饰器并确保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完全不会报错了

关键修改点说明

  1. 用协议强制目标函数的参数要求
    我定义了RequiresDepsParam协议,明确要求传入装饰器的函数必须接受deps关键字参数。这样mypy会自动检查所有被@Injector装饰的函数,如果没定义deps参数,就会直接报错,从源头保证类型安全。

  2. 修正装饰器的类型转换逻辑
    现在__call__的签名是func: RequiresDepsParam[P, T] -> Callable[P, T],表示:

    • 输入的函数func需要接受P的参数 + deps关键字参数
    • 输出的装饰后函数只需要接受P的参数(也就是自动帮你去掉了deps参数的要求)
  3. 把目标函数的deps设为关键字-only参数
    在foo函数里用*, deps: dict[str, Any]的写法,把deps变成只能用关键字传入的参数。这和装饰器中deps=deps的传递方式完全一致,避免了位置参数顺序的混淆,也让mypy的检查更精准。

关于functools.wraps的说明

你完全不用担心functools.wraps的工作问题,它会正确复制原函数的所有元数据——包括函数名、文档字符串、参数列表等。装饰后的foo函数,无论是运行时的行为还是类型系统的认知,都会符合预期。

备注:内容来源于stack exchange,提问作者AsfhtgkDavid

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 18:48:01