为含*args与关键字-only参数的Callable标注类型遇mypy报错求解
解决类方法调用次数统计装饰器的Mypy类型不匹配问题
首先明确:typing.Callable无法直接指定关键字-only参数,必须通过typing.Protocol来定义符合要求的可调用类型。你之前用Protocol出错,大概率是没正确匹配类方法的签名(比如遗漏self参数,或者关键字-only参数的写法不对)。
下面是能让Mypy通过的完整实现方案:
1. 定义适配类方法的Protocol
先定义一个专门针对类方法的Protocol,明确包含self参数和关键字-only的log_name:
from typing import Protocol, TypeVar, Any, ParamSpec P = ParamSpec("P") R = TypeVar("R") class ClassMethodWithLogName(Protocol): def __call__(self, *args: P.args, log_name: str, **kwargs: P.kwargs) -> R: ...
这里用ParamSpec和TypeVar来泛化方法的参数和返回值,保证装饰器能适配不同签名的类方法。
2. 实现类型正确的装饰器
装饰器需要接收上述Protocol类型的方法,返回一个新的类方法,同时处理调用次数统计:
call_counts: dict[str, int] = {} def count_calls(method: ClassMethodWithLogName[P, R]) -> ClassMethodWithLogName[P, R]: def wrapper(self: Any, *args: P.args, log_name: str, **kwargs: P.kwargs) -> R: if log_name not in call_counts: call_counts[log_name] = 0 call_counts[log_name] += 1 return method(self, *args, log_name=log_name, **kwargs) return wrapper
这里wrapper严格遵循Protocol的签名:self作为第一个参数,log_name是关键字-only,其余参数用ParamSpec泛化。
3. 在类中使用装饰器
这样使用时Mypy不会报错,且log_name必须以关键字参数传入:
class MyService: @count_calls def process_data(self, data: list[int], log_name: str) -> bool: # 业务逻辑 return len(data) > 0 # 正确调用(必须用关键字传log_name) service = MyService() service.process_data([1,2,3], log_name="data_process") # 错误调用(位置传log_name会被Mypy报错) # service.process_data([1,2,3], "data_process")
关键说明
- 为什么不用普通Callable?因为Callable的语法
Callable[[Arg1, Arg2], Return]只能定义位置参数,无法标记关键字-only参数。 - Protocol的核心是匹配
__call__方法的签名:必须包含类方法的self,用log_name: str直接声明为关键字-only(也可以用*, log_name: str明确分隔位置参数和关键字-only参数,效果一致)。 - 用
ParamSpec和TypeVar是为了让装饰器不丢失原方法的参数和返回值类型信息,保证Mypy能正确推断。
如果你的装饰器需要支持静态方法或普通函数,只需调整Protocol的__call__签名(比如去掉self参数)即可。
内容的提问来源于stack exchange,提问作者Paulo-99
相关产品推荐
相关产品推荐

