You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为动态创建的类添加类型提示,实现mypy的正确静态校验

实现动态生成类的Mypy类型校验方案

你的需求完全可行,核心是让Mypy识别@task装饰器动态生成的Task和Inputs类的类型信息,以下是几个具体的尝试方向:

方向一:编写Mypy插件(最彻底的解决方案)

Mypy支持通过插件扩展静态分析能力,针对@task装饰器编写插件后,可在类型检查阶段解析原函数签名,为动态生成的类补充完整类型信息:

  1. 核心逻辑:

    • 拦截被@task装饰的函数,提取参数名、类型注解和默认值
    • 为动态生成的Task类构造__init__方法签名(包含name和原函数所有参数)
    • 为Task.inputs属性构造对应Inputs类的类型,包含原函数参数的类型注解
  2. 简化版插件框架:

    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
    
  3. 使用方式:
    在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 20:14:56