科技 · 实用指南
dataclasses.asdict 报 RecursionError?先选导出字段,再递归遍历
字段过滤器可能来不及执行。先区分递归类型、真实对象环和共享子对象,再决定 JSON 的表示方式。

数据类打印时看似正常,asdict() 沿子对象返回父对象时却可能失败。如果转换根本走不到生成字典这一步,事后删除 parent 字段便无济于事。应在递归遍历开始前,决定导出内容需要保留哪些关系。
我们在 Linux 的 CPython 3.12.14 上运行了 10 组小型对照,区分三种对象结构,验证过滤器一次也未调用的情况,并在不改变原父子链接的前提下完成扁平 ID 导出。这是一套有明确范围的诊断步骤,不是通用图序列化器。
1. 确认对象结构和实际失败步骤
记录解释器版本与完整异常堆栈,确定失败来自 asdict、自定义转换函数还是 json.dumps。在一次性示例中检查对象身份,避免递归打印整个图:child.parent is root 能证明这条反向引用,值相等本身不能证明身份相同。
测试中的有限 Node 链以 None 结束,可以正常转换。root 的 children 列表包含 child,而 child 又指回 root 时,asdict 抛出 RecursionError。Pair 的 left 与 right 指向同一个叶节点时没有环,转换得到值相等但身份不同的字典。不能把每次重复身份都当成环;关键在于对象是否重新出现在当前路径上。
2. 在向下遍历前排除字段
本次核对的 CPython 实现先递归转换字段值,再把键值对交给自定义 dict_factory。我们的工厂函数会增加计数并删除 parent 键,但两节点环已经触发 RecursionError,计数仍为零。先构造结果、再删除字段,也有同样的时序问题。
另一组对照把 parent 定义为 repr=False、compare=False,并设置 metadata={"serialize": False},asdict 仍然沿该字段递归。文档中的前两个选项分别控制自动生成的文本表示和比较;metadata 需要识别它的消费者。标准 asdict 不会自动采用自定义序列化标记,因此这些声明变化不能证明导出已经修复。
3. 分清复制与序列化策略
文档给出的浅提取写法 {f.name: getattr(obj, f.name) for f in fields(obj)} 可以避开 asdict 的递归转换,但在 child 示例中,parent 值仍是原 root 对象。把该字典交给带 default=vars 的 json.dumps 后,测试出现循环引用 ValueError。浅层字典适合检查,不会自动变成 JSON 树。
deepcopy(root) 创建了新对象,但新 child 仍指向复制后的 root。这是正确的图复制行为;用相同诊断 default 编码 JSON 时,环依然被拒绝。原作者文章 Deterministic hashing of Python data objects 早已展示在 JSON 递归前选择性浅提取字段。方法本身已有先例,需要明确的是本次导出应省略哪些关系、又应怎样显式表达其余关系。
4. 用稳定引用定义小型交换结构
在我们的两节点示例中,将每个节点只放入 nodes 数组一次,仅输出 id、parent_id 和 child_ids。根行是 {"id":"root","parent_id":null,"child_ids":["child"]},子行是 {"id":"child","parent_id":"root","child_ids":[]}。字符串保留关系,不会再次嵌入父对象。持久交换格式应采用稳定业务 ID,不要使用仅在当前进程有意义的 id(obj)。
从明确给出的节点列表直接构造这些行,不要先对循环模型调用 asdict。编码前拒绝重复 ID,以及不在声明节点集合中的父或子 ID;两种反例在测试中都被拒绝。该结构刻意保持简单:不连通节点、双向关系不一致、额外字段与数据可见范围,仍需由业务另行规定;编码成功不能替代这些决定。
5. 验证导出内容,并保留原模型
有效投影经过 json.dumps、json.loads 往返后,仍有两行及预期 ID 关系;child.parent is root 与 root.children[0] is child 也仍为真。对自己的示例重复检查,再验证接收方结构和引用解析。保留重复 ID 与缺失引用两个失败用例,避免后续重构悄悄丢失关系。
不要靠提高递归上限或关闭循环检查让真实对象环变成可序列化数据。已审读的 Dataclass Wizard 示例把有限嵌套字典载入递归类型模型,仅凭这些示例不能证明能导出运行中的身份环。本次没有执行第三方序列化器;结论限定在指定版本和 10 组小型对照,不覆盖所有版本或任意对象图。
