首页 > 解决方案 > 是否可以在 python 中使用 make_dataclass 创建递归数据类?

问题描述

这是一个简单的例子,我试图创建一个递归节点定义,其中包含一个可选的子节点,它也是一个节点。代码编译但是当我尝试访问我得到的类型定义时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。

标签: pythonpython-typingpython-dataclasses

解决方案


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

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

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

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

当您调用typing.get_type_hints一个类时,typing.get_type_hints将尝试通过查看定义该类的模块来解析字符串注释。它通过查看__module__类中的条目来确定该模块__dict__。因为您以一种不通过正常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在实践中使用,您可能不知道类名。您可能在函数中和/或循环中使用它。typing.get_type_hints必须能够通过匹配类名的全局变量找到类,但是动态变量名比较乱。

您可以采取简单的方法,即使用以下方法设置全局globals()

globals()[your_dataclass.__name__] = your_dataclass

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

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


推荐阅读