如何为Python中作为可调用对象的装饰器添加类型注解并实现全阶段类型校验
解决可调用装饰器的类型注解问题(兼顾装饰与调用阶段的类型校验)
你的问题核心在于实例方法在未绑定(类上的方法)和绑定(实例上的方法)状态下的类型差异,之前的方案要么无法适配这种差异导致mypy报错,要么需要重复定义参数导致冗余。下面是一种更简洁且类型安全的解决方案,利用ParamSpec和泛型Protocol来避免重复定义,同时保证装饰和调用阶段的类型校验都能正常工作。
完整解决方案代码
from typing import Any, Awaitable, Callable, ParamSpec, Protocol, TypeVar # 定义泛型变量:Self表示实例类型,P捕获方法的参数(除self外),R限制返回值为Awaitable[str] Self = TypeVar('Self') P = ParamSpec('P') R = TypeVar('R', bound=Awaitable[str]) # 定义Protocol,同时描述未绑定方法(带self)和绑定后的方法(通过__get__) class UnboundMethod(Protocol[Self, P, R]): def __call__(self, self_arg: Self, *args: P.args, **kwargs: P.kwargs) -> R: ... def __get__(self, instance: Self | None, owner: type[Self]) -> Callable[P, R] | Self: ... def some_decorator() -> Callable[[UnboundMethod[Self, P, R]], UnboundMethod[Self, P, R]]: def wrapper(func: UnboundMethod[Self, P, R]) -> UnboundMethod[Self, P, R]: print("Decorator is applied.") return func return wrapper class MyClass: @some_decorator() async def my_method(self, some_arg: int) -> str: return f"Result: {some_arg}" # 正确调用示例 if __name__ == "__main__": obj = MyClass() await obj.my_method(some_arg=1) # 类型正确,mypy无报错
方案说明
泛型变量的作用:
Self:用来表示类的实例类型,让Protocol可以适配任意类的实例方法P(ParamSpec):捕获方法除self外的所有参数,避免重复写参数列表,保证一致性R:限制方法返回值必须是Awaitable[str],符合你的需求
Protocol的设计:
__call__方法描述未绑定的实例方法(类上的方法,需要传入self作为第一个参数)__get__方法模拟Python的方法绑定机制:当从实例获取方法时,返回一个绑定后的Callable[P, R](不需要再传self),这样mypy就能正确识别实例调用时的参数类型
验证你的错误场景
场景1:调用时传入错误类型参数
obj = MyClass() obj.my_method("str") # mypy报错:Argument 1 has incompatible type "str"; expected "int"
场景2:定义方法时指定错误参数类型
class BadMyClass: @some_decorator() async def my_method(self, some_arg: str) -> str: # mypy报错:Argument 2 of "__call__" has incompatible type "str"; expected "int" return f"Result: {some_arg}"
这两种情况mypy都会给出准确的类型错误提示,完全符合你的需求。
为什么之前的方案有问题?
- 第一个方案的
MethodCallable只描述了绑定后的方法类型,而装饰器接收的是未绑定的实例方法(带self参数),类型不匹配导致mypy报错 - 第二个方案手动重复定义了
__call__和__get__的参数,不仅冗余,还容易出现参数不一致的问题,导致类型校验失效
这个方案通过泛型和ParamSpec彻底解决了这些问题,同时保持代码简洁且类型安全。
内容的提问来源于stack exchange,提问作者Pablo Estevez
相关产品推荐
相关产品推荐

