Python 装饰器类型体操

💡 原文英文,约800词,阅读约需3分钟。
📝

内容提要

文章介绍为Python装饰器添加类型提示的方法:简单装饰器可用TypeVar保持原函数签名;修改返回值时可用Callable和Awaitable标注;复杂参数可用多个TypeVar配合overload,或使用PEP-612的ParamSpec与Concatenate,但后者目前mypy支持不完善。

🔎

延伸解读

简单装饰器:用TypeVar保留原签名

对于不修改参数和返回值的装饰器(如日志装饰器),可以用绑定到Callable的TypeVar来标注。这样装饰后的函数仍保留原签名,mypy的reveal_type会显示具体参数和返回类型,而不是Any。文中通过cast将wrapper转为TCallable,解决了内部实现类型不匹配的问题。

修改返回值时:Callable与Awaitable标注

当装饰器改变返回类型(如同步转异步),可以用Callable[..., R]接收原函数,返回Callable[..., Awaitable[R]]。但参数类型会退化为*Any, **Any,失去具体参数检查。文中示例显示,即使传入错误类型(如two_sum(2, "3")),mypy也不会报错,说明这种写法只保证了返回类型正确。

复杂参数:多TypeVar配合overload

若想保留参数类型,可以为不同参数个数分别定义TypeVar和overload。文中展示了1到5个参数的版本,并提供了生成代码的脚本。这种方法能精确标注每个参数,但需要手动扩展,参数越多代码越冗长,且仍无法覆盖关键字参数或可变参数的情况。

PEP-612的ParamSpec与Concatenate:理想但支持有限

ParamSpec和Concatenate能优雅地处理装饰器参数类型,例如在参数列表前插入Context。但文章指出,目前mypy对PEP-612的支持并不完善,因此实际项目中可能无法完全依赖。读者需关注所用类型检查器的版本和兼容性,谨慎选择方案。

❓

Q&A

如何给一个不修改函数签名和返回值的简单装饰器添加类型提示?

可以使用 TypeVar 并绑定到 Callable,例如 TCallable = TypeVar("TCallable", bound=Callable),装饰器函数定义为 def add_log(f: TCallable) -> TCallable,内部用 cast(TCallable, wrapper) 返回包装函数,这样能保持原函数的签名。

如果装饰器要修改函数的返回值(比如把同步函数变成异步),类型提示应该怎么写?

可以用 Callable[..., R] 和 Awaitable[R] 来标注,例如 def sync_to_async(f: Callable[..., R]) -> Callable[..., Awaitable[R]],并用 cast 转换 wrapper 的类型。但这样会丢失参数类型信息,参数变成 *Any, **Any。

有没有办法在装饰器中保留原函数的参数类型,而不是变成 Any?

有两种方法:一是用多个 TypeVar 配合 @overload 为不同参数数量的函数生成重载签名;二是使用 PEP-612 的 ParamSpec 和 Concatenate,但后者目前 mypy 支持不完善。

用多个 TypeVar 和 overload 实现装饰器类型提示的具体做法是什么?

定义多个 TypeVar(如 A, B, C, D, E)和返回类型 RV,然后为不同参数数量的函数编写 @overload 声明,例如 Callable[[A], RV] -> Callable[[A], Awaitable[RV]],最后实现函数。可以用脚本生成这些重载代码。

PEP-612 的 ParamSpec 和 Concatenate 在装饰器类型提示中怎么用?

可以用 ParamSpec 捕获参数列表,Concatenate 添加额外参数。例如 def with_context(f: Callable[Concatenate[Context, P], R]) -> Callable[P, R],内部用 *args: P.args, **kwargs: P.kwargs 标注。但 mypy 目前不完全支持。

为什么我的装饰器加了类型提示后,被装饰的函数类型变成了 Any?

因为装饰器本身没有类型提示,mypy 无法推断其返回类型,导致被装饰函数被推断为 Any。需要给装饰器添加类型提示,例如使用 TypeVar 或 Callable 标注。

🏷️

标签

➡️

继续阅读