是否可以使用 python 中的 make_dataclass 创建递归数据类?

Is it possible to create a recursive dataclass using make_dataclass in python?

这是一个简单的例子,我试图创建一个递归节点定义,其中包含一个可选的子节点,它也是一个节点。代码可以编译,但是当我尝试访问类型定义时,我得到 node 未定义。是否可以解决此错误?

import dataclasses
import typing as t

node_type = dataclasses.make_dataclass(
    "node", [("child", t.Optional["node"], dataclasses.field(default=None))]
)
print(t.get_type_hints(node_type))

产出

NameError: name 'node' is not defined

我正在使用 python 3.9.2.

这里存在三个问题。它们是可以解决的,但在您实际使用 dataclasses.make_dataclass.

的情况下,它们可能无法完全解决

第一个问题是typing.get_type_hints正在寻找一个名为'node'的class,但是你调用了全局变量node_type。您传递给 make_dataclass 的名称、您在注释中使用的名称以及您分配数据 class 的名称都必须相同:

Node = dataclasses.make_dataclass(
    "Node", [("child", t.Optional["Node"], dataclasses.field(default=None))]
)

但这还不够,因为 typing.get_type_hints 没有在正确的命名空间中查找。这是第二个问题。

当您在 class 上调用 typing.get_type_hints 时,typing.get_type_hints 将尝试通过查看定义 class 的模块来解析字符串注释。它通过查看 class 的 __dict__ 中的 __module__ 条目来确定该模块。因为您以不通过正常 class 语句的奇怪方式创建了节点 class,所以 class 的 __module__ 未设置为参考正确的模块。相反,它设置为 'types'.

您可以通过手动将 __module__ 预先设置为当前模块的 __name__ 来解决此问题:

Node = dataclasses.make_dataclass(
    "Node",
    [("child", t.Optional["Node"], dataclasses.field(default=None))],
    namespace={'__module__': __name__}
)

那么typing.get_type_hints就可以解析字符串注解了

元问题是,如果您在实践中使用 dataclasses.make_dataclass,您可能不知道 class 名称。您可能在函数中使用它,and/or 在循环中。 typing.get_type_hints 必须能够通过匹配 class 名称的全局变量找到 class,但是动态变量名称很乱。

您可以采用简单的方法,只需使用 globals():

设置全局变量
globals()[your_dataclass.__name__] = your_dataclass

但这很危险。如果生成的两个 classes 具有相同的名称,则第二个将替换第一个。如果生成的 class 与全局命名空间中的其他名称同名,例如如果您执行了 from some_dependency import Thing 然后生成了名为 Thing 的 class,则生成的 class 将踩踏现有的全局值。

如果你能保证那些事情不会发生,globals()可能没问题。如果你不能做出这样的保证,你可能需要做一些事情,比如为每个生成的 class 生成一个新模块来居住,这样他们每个人都有自己独立的全局命名空间,或者你可能只是接受并记录事实上 get_type_hints 不适用于您生成的 classes.