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

使用装饰器转换函数参数类型的类型注解问题求助

类型转换装饰器的类型注解问题

我需要实现一个装饰器,自动将函数参数中类型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

注解说明

  1. 让装饰器的输入和输出函数共享同一个ParamSpec P,表示参数列表结构完全一致,仅其中一个A类型参数被替换为B类型。
  2. 使用cast(P.args)明确告知Pyright:转换后的参数列表符合被装饰函数的参数类型要求——我们已经将A类型参数转换为了被装饰函数期望的B类型。
  3. 该方案不需要提前知道目标参数的位置,完全适配动态查找转换的逻辑。

测试示例用例时,Pyright会正确识别:装饰后的write函数接受api.Context类型的ctx参数,而函数内部实际处理的是UserContext类型。

内容的提问来源于stack exchange,提问作者YousefZ

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 06:03:17