使用装饰器转换函数参数类型的类型注解问题求助
类型转换装饰器的类型注解问题
我需要实现一个装饰器,自动将函数参数中类型A的实例转换为类型B,但在给这个装饰器添加类型注解时遇到了Pyright类型检查错误。
初始实现及错误
我的初始代码如下:
from typing import TypeVar, ParamSpec, Callable, Coroutine, Any, Type A = TypeVar("A") B = TypeVar("B") T = TypeVar("T") P = ParamSpec("P") P2 = ParamSpec("P2") def ensure(a: Type[A], convert: Callable[[A], B] ) -> Callable[[Callable[P2, Coroutine[Any, Any, T]]], Callable[P, Coroutine[Any, Any, T]]]: def command(func: Callable[P2, Coroutine[Any, Any, T]]) -> Callable[P, Coroutine[Any, Any, T]]: async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: for i, argument in enumerate(args): if isinstance(argument, a): new_instance = convert(argument) arguments = (*args[:i], new_instance, *args[i + 1:]) return await func(*arguments, **kwargs) raise TypeError("Type not found in arguments") return wrapper return command
运行Pyright时出现以下错误:
error: ParamSpec "P" is scoped to a callable within the return type and cannot be referenced in the function body (reportGeneralTypeIssues)error: Arguments for ParamSpec "P2@command" are missing (reportGeneralTypeIssues)
尝试使用Concatenate的方案
我尝试用Concatenate来处理参数,但问题是我无法提前知道目标参数的位置:
from typing import TypeVar, ParamSpec, Callable, Coroutine, Any, Type, Concatenate A = TypeVar("A") B = TypeVar("B") T = TypeVar("T") P = ParamSpec("P") def ensure(a: Type[A], convert: Callable[[A], B] ) -> Callable[[Callable[Concatenate[B, P], Coroutine[Any, Any, T]]], Callable[Concatenate[A, P], Coroutine[Any, Any, T]]]: def command(func: Callable[Concatenate[B, P], Coroutine[Any, Any, T]]) -> Callable[Concatenate[A, P], Coroutine[Any, Any, T]]: async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: for i, argument in enumerate(args): if isinstance(argument, a): new_instance = convert(argument) arguments = (*args[:i], new_instance, *args[i + 1:]) return await func(*arguments, **kwargs) raise TypeError("Type not found in arguments") return wrapper return command
示例用例
我的实际使用场景如下:
class UserContext(api.Context): def __init__(self, ctx: api.Context): ... @api.command() @ensure(api.Context, UserContext) async def write(ctx: UserContext, ...): ...
API框架会调用write并传入api.Context实例,装饰器需要将其转换为UserContext后再传给write函数。
正确的类型注解方案
要解决这个问题,我们需要让装饰器的类型注解表达清楚:被装饰的函数接受包含B类型的参数,而装饰后的函数接受包含A类型的参数,且参数列表的其余部分完全一致。这里可以通过正确约束ParamSpec的作用域并配合类型转换断言实现:
from typing import TypeVar, ParamSpec, Callable, Coroutine, Any, Type, cast A = TypeVar("A") B = TypeVar("B") # 函数返回值类型 R = TypeVar("R") # 函数的参数规格 P = ParamSpec("P") def ensure(a: Type[A], convert: Callable[[A], B]) -> Callable[[Callable[P, Coroutine[Any, Any, R]]], Callable[P, Coroutine[Any, Any, R]]]: def command(func: Callable[P, Coroutine[Any, Any, R]]) -> Callable[P, Coroutine[Any, Any, R]]: async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: for i, arg in enumerate(args): if isinstance(arg, a): converted_arg = convert(arg) # 用cast告诉类型检查器,转换后的参数列表符合被装饰函数的要求 new_args = cast(P.args, (*args[:i], converted_arg, *args[i+1:])) return await func(*new_args, **kwargs) raise TypeError("Type not found in arguments") return wrapper return command
注解说明
- 让装饰器的输入和输出函数共享同一个
ParamSpec P,表示参数列表结构完全一致,仅其中一个A类型参数被替换为B类型。 - 使用
cast(P.args)明确告知Pyright:转换后的参数列表符合被装饰函数的参数类型要求——我们已经将A类型参数转换为了被装饰函数期望的B类型。 - 该方案不需要提前知道目标参数的位置,完全适配动态查找转换的逻辑。
测试示例用例时,Pyright会正确识别:装饰后的write函数接受api.Context类型的ctx参数,而函数内部实际处理的是UserContext类型。
内容的提问来源于stack exchange,提问作者YousefZ
相关产品推荐
相关产品推荐

