DEBUG.WIKI
阿里云;从建站到云上业务;云服务器 · 数据库 · 对象存储;查看产品 →
OnlineSIM:在线接收短信,虚拟号码租用
RackNerd;$21.99/年 · 1GB KVM

attrs 构造签名解析 Foo 失败,显式提供注解命名空间

生成的 attrs 构造函数可能使用与业务模块不同的全局命名空间。对字符串注解调用 inspect.signature 时显式传 globals,并区分构造签名与 attrs 字段类型的解析。

模块里已经定义了 Foo,inspect.signature(Model, eval_str=True) 仍报 NameError: name 'Foo' is not defined,而同样注解的手写类正常。如果 Model 是 attrs 生成构造函数的类,应检查反射时实际使用的命名空间。

对于使用 from __future__ import annotations 的模块,注解可以仍是字符串。手写函数的全局变量通常属于业务模块,生成的 __init__ 则可能使用为生成代码准备的字典;类定义完成后才出现的 Foo,不一定被那个字典看到。Python 3.13、3.14 都有这类具体表现。

字符串注解可以交给显式命名空间解析

下面是包含前向引用的独立结构示例;解析应在相关类型都已经定义以后进行:

from __future__ import annotations
import inspect
from attrs import define

@define
class Model:
    _foo: Foo

class Foo:
    pass

signature = inspect.signature(
    Model, globals=globals(), eval_str=True
)
print(signature)

globals() 要来自定义这些业务类型的模块。若反射代码在另一个模块中,不应把工具模块自己的 globals() 传进去;应由类型拥有方提供已知命名空间。类定义在函数局部时,还需要保留并显式提供相应 locals,不能依赖函数退出后那些局部名字仍可找到。

解析字符串注解可能执行其中的表达式,只应反射可信代码。不要为了解析输入数据携带的任意类型表达式,把它们塞进这个接口执行。

字段类型与构造函数签名是两个读取入口

如果消费方真正需要的是 attrs 字段类型,可以在类型定义齐全后调用 attrs.resolve_types(Model, globalns=...),再从 attrs.fields(Model) 读取。这个接口更新字段的 type 信息,不应据此宣称已经重建了生成构造函数的所有注解。

Python 3.14 不使用 future 注解时,还可能看到真实的 ForwardRef 对象或包含它的泛型。eval_str=True 不是任意注解对象的通用递归求值器;不能把上面的字符串方案直接套上,再断言 list[ForwardRef(...)] 已全部解决。对于这个分支,使用支持该版本注解协议的消费者,或在类声明前定义可提前定义的类型。

复核要检查最终得到的对象是否真是业务 Foo,而不是只看打印出的名字相同。保留私有字段去掉前导下划线后的构造参数名,也不要为修复反射把 _foo 的存储名称改掉。