テクノロジー · 実用ガイド

dataclasses.asdict で RecursionError?再帰前に出力フィールドを選ぶ

フィールドの除外が間に合わない場合があります。再帰型、実際の参照循環、共有される子を区別して JSON の形を決めます。

有限連鎖、逆参照による循環、循環のない共有子の比較
独自の説明用イラスト:実験結果を分けるのは再帰型注釈だけでなく、実際の参照構造です。 Watch at Ease

表示は正常な dataclass でも、asdict() が子から親へ戻る参照をたどると失敗することがあります。辞書が完成しないなら、完成後に parent を削除する処理は間に合いません。再帰処理に入る前に、出力へ含める関係を決めます。

Linux の CPython 3.12.14 で10件の小さな対照実験を実行しました。3種類の構造、呼び出されないフィルター、元の親子関係を変えないフラットな ID 表現を確認しています。これは範囲を限定した診断手順で、汎用グラフシリアライザーではありません。

1. 構造と失敗した処理を確認する

インタープリターのバージョンと完全なトレースバックを記録し、asdict、独自ヘルパー、json.dumps のどこで失敗したかを分けます。使い捨ての例ではグラフ全体を再帰表示せず、同一性を調べます。child.parent is root はその逆参照を示しますが、値が等しいだけでは同一オブジェクトとは限りません。

有限の Node 連鎖は None で終わり、正常に変換できました。root.children に child が入り、child が root を参照すると RecursionError になりました。一方、Pair の left と right が同じ葉を共有しても循環はなく、値は等しく同一性は異なる辞書が出力されました。重複参照すべてを循環と見なさず、現在の経路に戻るかを区別します。

2. dict_factory より前に除外する

確認した CPython の実装は、フィールド値を再帰変換した後で独自 dict_factory にペアを渡します。実験のファクトリーはカウンターを増やし parent キーを除くものでしたが、2ノードの循環で RecursionError となり、カウンターは0のままでした。完成した結果から削除する方法にも同じ順序の問題があります。

別の対照では 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) は新しいオブジェクトを作りましたが、コピーされた子はコピーされた親を参照しました。グラフのコピーとしては正しくても、同じ診断用 default を使った JSON 化は循環を拒否しました。原著記事 Deterministic hashing of Python data objects は既に、JSON の再帰前に選択的な浅い取り出しを行う方法を示しています。方法の新規性ではなく、どの関係を省略し、どれを明示するかが判断点です。

4. 安定した ID で小さな交換形式を定義する

2ノードの例では各ノードを 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 で往復すると、2行と必要な ID 関係が維持されました。child.parent is root と root.children[0] is child も真のままでした。自分の例でも確認し、受信側のスキーマと参照解決を検証します。重複 ID と参照先欠落の失敗例を残し、改修で関係が失われないようにします。

実際の循環を出力するために再帰上限を上げたり、循環検査を無効にしたりしないでください。読んだ Dataclass Wizard の例は有限の入れ子辞書から再帰型モデルを作るもので、それだけで実行時の同一オブジェクト循環を出力できるとは言えません。第三者のシリアライザーは実行していません。結果は指定ビルドと10件の小さな実験に限定され、全バージョンや任意のグラフを保証しません。

親子を一度ずつ出力し ID 参照と検証を付けた図
独自の説明用イラスト:アプリケーションの関係をたどる前に交換形式を定義します。 Watch at Ease