如何创建可被类型检查器识别的带类型注解的自动生成函数?
我想模仿dataclasses、Pydantic的BaseModel这类Python模块的逻辑——它们能自动生成带特定类型注解的方法,而且这些注解能被MyPy这类静态类型检查器识别,给出类型提示。
比如用dataclass定义类:
from dataclasses import dataclass @dataclass class Test: a: int b: str
它会自动生成__init__方法,这个方法的__annotations__是{"a": int, "b": str},MyPy就能据此检查Test实例的构造是否符合类型要求。
现在我不想依赖dataclasses,自己实现类似功能:指定注解来创建函数,比如下面的例子,我希望MyPy能标记传入字符串的错误,但直接赋值__annotations__的方式静态检查器根本识别不了:
annotations = {"parameter": float} def f(parameter) -> None: print(f"f is called with {parameter}") f.__annotations__ = annotations f("test") # 本来期望MyPy报错,但实际没反应
我找过类似问题的解决方案,都没法被静态类型检查器识别,也啃了dataclass的源码,但还是没搞明白怎么实现。
解决方案
一、函数场景:用装饰器生成静态可识别的注解函数
直接赋值__annotations__是运行时操作,静态检查器分析代码时看不到这个赋值,自然没法识别类型约束。要让检查器生效,得在定义阶段就让注解被解析器抓到,试试用装饰器结合typing工具来实现:
from typing import Callable, TypeVar, ParamSpec P = ParamSpec("P") R = TypeVar("R") def add_annotations(annotations: dict[str, type]) -> Callable[[Callable[P, R]], Callable[P, R]]: def decorator(func: Callable[P, R]) -> Callable[P, R]: # 运行时更新函数注解 func.__annotations__.update(annotations) # 核心:让静态检查器能识别注解,得靠类型提示的兼容写法 return func return decorator # 用起来是这样 @add_annotations({"parameter": float}) def f(parameter) -> None: print(f"f is called with {parameter}") f("test") # MyPy会提示:Argument 1 to "f" has incompatible type "str"; expected "float"
如果上面的装饰器在静态检查时还是不够精准,也可以直接动态生成带注解的函数源码——让静态检查器能直接解析到类型:
import types from typing import float def create_function_with_annotations(annotations: dict[str, type], func_body): # 动态生成带注解的函数源码 param_str = ", ".join([f"{name}: {anno.__name__}" for name, anno in annotations.items()]) # 这里假设返回值是None,按需调整就行 func_code = f"def f({param_str}) -> None:\n {func_body}" local_names = {} exec(func_code, globals(), local_names) return local_names["f"] # 使用示例 def func_body(parameter): print(f"f is called with {parameter}") f = create_function_with_annotations({"parameter": float}, func_body.__code__) f("test") # MyPy能识别到类型错误
这种方式靠动态生成带注解的源码让检查器识别,缺点就是动态代码可读性差点。
二、类场景:用元类生成带注解的__init__(仿dataclass)
如果是要实现类似dataclass自动生成带注解的__init__,元类是合适的选择——元类能在类定义阶段修改类属性,而且静态检查器能识别元类生成的注解:
from typing import Any class AutoInitMeta(type): def __new__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]) -> type: # 提取类里的字段注解(排除已有的方法) field_annotations = { k: v for k, v in namespace.get("__annotations__", {}).items() if k not in namespace } if field_annotations: # 生成带类型注解的__init__源码 params = ", ".join([f"{name}: {anno.__name__}" for name, anno in field_annotations.items()]) init_code = f""" def __init__(self, {params}): {'; '.join([f'self.{name} = {name}' for name in field_annotations.keys()])} """ local_names = {} exec(init_code, globals(), local_names) namespace["__init__"] = local_names["__init__"] # 同步更新__init__的注解 namespace["__init__"].__annotations__ = field_annotations.copy() namespace["__init__"].__annotations__["return"] = None return super().__new__(cls, name, bases, namespace) # 使用示例 class Test(metaclass=AutoInitMeta): a: int b: str test = Test("123", "hello") # MyPy会提示:Argument 1 to "Test" has incompatible type "str"; expected "int"
这个元类在类定义时,会根据类的字段注解生成带类型注解的__init__方法,静态检查器能解析到这些注解,自然就能做类型检查了。
核心逻辑
MyPy这类静态检查器是基于源码静态分析的,任何只在运行时修改__annotations__的操作,检查器都看不到。要让它生效,要么把类型注解写在静态源码里,要么用检查器能识别的机制(比如装饰器的类型标注、元类生成的可解析代码)来传递类型信息。
内容的提问来源于stack exchange,提问作者ArielAlon24

