如何为动态创建的类添加类型提示,实现mypy的正确静态校验
实现动态生成类的Mypy类型校验方案
你的需求完全可行,核心是让Mypy识别@task装饰器动态生成的Task和Inputs类的类型信息,以下是几个具体的尝试方向:
方向一:编写Mypy插件(最彻底的解决方案)
Mypy支持通过插件扩展静态分析能力,针对@task装饰器编写插件后,可在类型检查阶段解析原函数签名,为动态生成的类补充完整类型信息:
核心逻辑:
- 拦截被
@task装饰的函数,提取参数名、类型注解和默认值 - 为动态生成的
Task类构造__init__方法签名(包含name和原函数所有参数) - 为
Task.inputs属性构造对应Inputs类的类型,包含原函数参数的类型注解
- 拦截被
简化版插件框架:
from mypy.plugin import Plugin, DecoratorContext from mypy.nodes import Decorator, FuncDef from mypy.types import Instance, Type class TaskPlugin(Plugin): def get_decorator_hook(self, fullname: str): if fullname == "your_module.task": return self.task_decorator_hook return None def task_decorator_hook(self, ctx: DecoratorContext) -> Type: func = ctx.decorated if not isinstance(func, FuncDef): return ctx.default_return_type # 构造Inputs类的类型实例 inputs_fields = {p.name: p.type for p in func.args} inputs_type = Instance( type_alias=ctx.api.named_type("your_module.InputsTemplate"), args=[], fields=inputs_fields ) # 构造Task类的类型 task_type = Instance( type_alias=ctx.api.named_type("your_module.FunctionTask"), args=[], fields={"inputs": inputs_type} ) # 补充__init__方法的类型签名(细节省略) return task_type def plugin(version: str): return TaskPlugin使用方式:
在mypy.ini中配置插件路径:[mypy] plugins = your_plugin_module.py
方向二:利用泛型+类型变量推导(无需插件,代码层面优化)
通过Python的typing.ParamSpec和TypeVar捕获原函数参数信息,让Mypy通过泛型推导动态生成类的类型:
优化后的装饰器代码
import typing as ty import attrs import inspect from functools import wraps P = ty.ParamSpec("P") R = ty.TypeVar("R") @attrs.define(kw_only=True, slots=False) class FunctionTask(ty.Generic[P]): name: str inputs: ty.Any def __init__(self, name: str, **kwargs: ty.Unpack[P]): self.name = name self.inputs = type(self).Inputs(**kwargs) def task(function: ty.Callable[P, R]) -> ty.Type[FunctionTask[P]]: sig = inspect.signature(function) params = list(sig.parameters.values()) # 构造Inputs类的字段与注解 inputs_attrs = {} inputs_annotations = {} for p in params: default = p.default if p.default is not inspect.Parameter.empty else attrs.NOTHING inputs_attrs[p.name] = attrs.field(default=default) inputs_annotations[p.name] = p.annotation Inputs = attrs.define( type("Inputs", (), {**inputs_attrs, "__annotations__": inputs_annotations}) ) @wraps(function, updated=()) @attrs.define(kw_only=True, slots=True, init=False) class Task(FunctionTask[P]): func: ty.Callable[P, R] = staticmethod(function) Inputs: ty.Type[ty.Any] = Inputs inputs: Inputs = attrs.field() def __call__(self) -> R: return self.func( **{f.name: getattr(self.inputs, f.name) for f in attrs.fields(self.Inputs)} ) # 为__init__绑定类型提示 Task.__init__ = ty.cast(ty.Callable[[ty.Any, str, ty.Unpack[P]], None], Task.__init__) return ty.cast(ty.Type[FunctionTask[P]], Task)
使用效果
@task def myfunc(x: int, y: int) -> int: return x + y # Mypy会校验x、y的类型 mytask = myfunc(name="mytask", x=1, y=2) # Mypy会捕获类型错误:字符串赋值给int类型 mytask.inputs.x = "bad-value"
方向三:静态代码生成(牺牲少量动态性,换最佳类型支持)
在装饰器中生成对应Task和Inputs类的静态代码,通过exec执行,让Mypy直接解析完整类型注解:
示例代码
import typing as ty import attrs import inspect from functools import wraps def task(function: ty.Callable) -> ty.Type[ty.Any]: sig = inspect.signature(function) params = list(sig.parameters.values()) func_name = function.__name__ task_name = f"{func_name}Task" inputs_name = f"{func_name}Inputs" # 生成Inputs类代码 inputs_fields = [] for p in params: default_str = f" = {repr(p.default)}" if p.default is not inspect.Parameter.empty else "" inputs_fields.append(f" {p.name}: {p.annotation.__name__}{default_str}") inputs_code = f""" @attrs.define(kw_only=True) class {inputs_name}: {chr(10).join(inputs_fields)} """ # 生成Task类代码 task_code = f""" @wraps({func_name}, updated=()) @attrs.define(kw_only=True, slots=True, init=False) class {task_name}(FunctionTask): func = staticmethod({func_name}) Inputs = {inputs_name} inputs: Inputs = attrs.field() def __call__(self): return self.func(**{{f.name: getattr(self.inputs, f.name) for f in attrs.fields(self.Inputs)}}) """ # 执行生成的代码 local_vars = {} exec(inputs_code, globals(), local_vars) exec(task_code, globals() | local_vars, local_vars) return local_vars[task_name]
优势
Mypy能直接解析生成的静态类代码,完全和你提供的StaticTask示例拥有相同的类型校验能力,无需额外插件。
总结
- 追求彻底动态性:选择Mypy插件方案
- 希望代码层面实现、无需额外配置:选择泛型+类型变量方案
- 优先保证类型校验效果:选择静态代码生成方案
内容的提问来源于stack exchange,提问作者Tom Close
相关产品推荐
相关产品推荐

