如何为Python装饰器编写适配类方法实例调用的泛型类型提示?
解决类方法使用装饰器时的mypy类型检查错误
问题分析
你编写的装饰器支持普通函数的直接调用或线程执行,但应用于类方法时,mypy会报参数类型不匹配错误。核心原因是:装饰器的ParamSpec(P)包含了方法的self参数,当方法绑定到实例后,self已被隐式传入,但mypy仍认为start_thread需要显式传入self,导致参数类型校验失败。
修改后的装饰器代码
通过拆分绑定/未绑定方法的类型处理,实现符合mypy预期的类型提示:
from typing import Callable, Generic, TypeVar, ParamSpec from functools import wraps from threading import Thread P = ParamSpec('P') R = TypeVar('R') InstanceT = TypeVar('InstanceT') class CallOrThreadFunction(Generic[InstanceT, P, R]): def __init__(self, start_thread: Callable[P, Thread], call: Callable[P, R]): self.start_thread = start_thread self.call = call def __get__(self, instance: InstanceT, owner: type[InstanceT] | None = None) -> 'BoundCallOrThreadFunction[P, R]': # 绑定实例时返回适配后的类型,自动移除self参数 bound_call = self.call.__get__(instance, owner) bound_start_thread = self.start_thread.__get__(instance, owner) return BoundCallOrThreadFunction(bound_start_thread, bound_call) class BoundCallOrThreadFunction(Generic[P, R]): def __init__(self, start_thread: Callable[P, Thread], call: Callable[P, R]): self.start_thread = start_thread self.call = call def call_or_start_thread(function: Callable[P, R]) -> CallOrThreadFunction[object, P, R]: @wraps(function) def start_thread(*args: P.args, **kwargs: P.kwargs) -> Thread: thread = Thread(target=function, args=args, kwargs=kwargs, daemon=True) thread.start() return thread return CallOrThreadFunction(start_thread, function)
关键修改点
- 新增类型变量与类拆分:
- 引入
InstanceT表示类实例类型,区分未绑定方法(类层面)和已绑定方法(实例层面)的类型。 - 拆分出
BoundCallOrThreadFunction专门处理实例绑定后的方法类型,此时参数列表自动移除self。
- 引入
- 实现
__get__方法:
当实例访问装饰后的方法时,通过__get__返回绑定后的类型,让mypy正确识别参数列表(不再要求传入self)。 - 修正线程返回值:
原代码错误返回Thread类,现在改为返回创建的Thread实例,符合实际逻辑和类型规范。
使用示例(mypy可通过校验)
class Employee: def __init__(self, name: str): self.name = name @call_or_start_thread def my_method(self, parameter: int = 1): print(f'name: {self.name} parameter: {parameter}') adrian = Employee('Adrian') adrian.my_method.start_thread(5) # 实例调用,mypy无报错 adrian.my_method.call(10) # 直接调用方法 Employee.my_method.start_thread(adrian, 6) # 类层面调用仍支持
内容的提问来源于stack exchange,提问作者Matt Kimberling
相关产品推荐
相关产品推荐

