Watch at Ease세상을 읽고, 일상의 답을 찾다.

기술 · 실용 가이드

dataclasses.asdict에서 RecursionError? 순회 전에 내보낼 필드를 고르세요

필드 필터가 실행되기 전에 실패할 수 있습니다. 재귀 타입, 실제 객체 순환, 공유 자식을 구분한 뒤 JSON 표현을 정하세요.

유한 연결, 실제 역참조 순환, 순환 없는 공유 자식 비교
자체 제작 설명 일러스트: 재귀 타입 주석뿐 아니라 실제 객체 구조가 실험 결과를 결정합니다. Watch at Ease

dataclass를 출력할 때는 멀쩡해 보여도 asdict()가 자식에서 부모로 돌아가는 참조를 따라가면 실패할 수 있습니다. 변환이 사전을 완성하지 못한다면 결과에서 parent를 지우는 방법은 늦습니다. 재귀 순회가 시작되기 전에 어떤 관계를 내보낼지 정해야 합니다.

Linux의 CPython 3.12.14에서 작은 대조 실험 10개를 실행했습니다. 객체 구조 세 가지와 호출되지 않는 필터를 구분하고, 원래 부모·자식 연결을 바꾸지 않는 평탄한 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 키를 제거하도록 했지만, 두 노드의 순환에서 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로 작은 교환 구조를 정하세요

두 노드 예제는 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개에 한정되며 모든 버전이나 임의의 그래프를 보장하지 않습니다.

부모와 자식을 한 번씩 내보내고 ID 연결을 검증하는 구조
자체 제작 설명 일러스트: 애플리케이션 관계를 순회하기 전에 교환 형식을 정합니다. Watch at Ease