파이썬 dataclass에서 list=[]나 dict={}를 기본값으로 넣어 mutable default 오류가 발생했다면, 인스턴스마다 새 객체가 필요한 필드에는 field(default_factory=...)를 사용하면 됩니다. 함수 인자의 기본값을 다루는 상황은 None을 받은 뒤 함수 안에서 새 객체를 만드는 방식으로 구분하고, 클래스 전체가 공유해야 하는 값만 ClassVar로 표시합니다.
왜 mutable default 오류가 발생할까
list, dict, set은 내용을 바꿀 수 있는 mutable 객체입니다. 이런 객체를 클래스 정의 시점의 기본값으로 두면 여러 인스턴스가 같은 객체를 바라볼 수 있습니다. 한 작업의 상태를 바꿨는데 다른 작업의 목록까지 달라지는 이유가 여기에 있습니다.
Python 3.11부터 dataclass는 해시할 수 없는 기본값을 mutable로 간주해 ValueError를 발생시킵니다. 따라서 아래처럼 작성한 코드는 버전에 따라 오류 문구가 달라질 수 있지만, 설계상으로도 피하는 편이 안전합니다.
from dataclasses import dataclass
@dataclass
class Job:
tags: list[str] = []
인스턴스마다 새 리스트가 필요하면 default_factory
작업 객체마다 독립된 목록이나 설정 사전이 필요하다면 field(default_factory=list) 또는 field(default_factory=dict)를 사용합니다. default_factory에는 인자 없이 호출할 수 있는 callable을 지정하며, 객체가 만들어질 때 기본값을 생성합니다.
from dataclasses import dataclass, field
@dataclass
class Job:
name: str
tags: list[str] = field(default_factory=list)
options: dict[str, str] = field(default_factory=dict)
first = Job("daily-report")
second = Job("weekly-report")
first.tags.append("retry")
print(first.tags) # ['retry']
print(second.tags) # []
default와 default_factory는 동시에 지정할 수 없습니다. 빈 목록 자체가 아니라 호출할 때마다 새 목록을 만들겠다는 의도가 필요하므로, list()를 미리 호출해 넣는 방식이 아니라 factory를 넘겨야 합니다.
함수 기본 인자는 None 초기화로 분리
dataclass 필드가 아니라 함수가 매번 새 목록을 받아야 하는 문제라면 같은 원칙을 함수 안에서 표현합니다. 함수 기본값은 함수 정의 시 한 번 만들어지므로 mutable 객체를 직접 넣지 않고 None을 기본값으로 둡니다.
def add_tag(tag: str, tags: list[str] | None = None) -> list[str]:
if tags is None:
tags = []
tags.append(tag)
return tags
이 방식은 호출자가 목록을 전달했을 때 그 목록을 계속 사용할지, 전달하지 않았을 때만 새 목록을 만들지를 코드에 분명히 남깁니다. dataclass의 필드 선언에는 default_factory, 함수의 매개변수에는 None 초기화를 우선 검토하면 판단이 쉬워집니다.
공유가 목적이면 ClassVar
모든 인스턴스가 함께 보는 등록표나 상수처럼 클래스 전체가 공유해야 하는 값이라면 무조건 default_factory로 복사할 필요는 없습니다. 그 값이 dataclass 인스턴스마다 달라지는 필드가 아니라는 의도를 ClassVar로 표시합니다.
from dataclasses import dataclass
from typing import ClassVar
@dataclass
class Job:
name: str
allowed_statuses: ClassVar[set[str]] = {"ready", "done"}
ClassVar로 표시한 속성은 dataclass 필드 처리에서 제외됩니다. 따라서 인스턴스별 입력 상태와 클래스 전체의 공통 정책을 섞지 않도록, 공유가 정말 의도된 경우에만 사용해야 합니다.
선택 기준을 한 번에 정리하면
- dataclass 인스턴스마다 새
list·dict·set이 필요하다면field(default_factory=...)를 선택합니다. - 함수 호출마다 새 mutable 값을 만들고 싶다면 매개변수 기본값을
None으로 두고 함수 안에서 생성합니다. - 모든 인스턴스가 함께 봐야 하는 클래스 수준의 값이라면
ClassVar를 검토합니다.
자동화 작업 상태, API 요청 데이터, 설정 객체처럼 서로 독립적으로 처리해야 하는 값은 대개 첫 번째 경우입니다. 반대로 공유를 의도하지 않았는데 ClassVar를 붙이면 오류를 숨기는 방향이 될 수 있습니다. 먼저 그 값이 인스턴스별 상태인지, 호출별 임시값인지, 클래스 공통 정책인지부터 정한 뒤 문법을 선택하는 것이 안전합니다.
적용 전에 확인할 것
기존 코드에서 오류를 고칠 때는 기본값 선언만 바꾸지 말고, 객체 생성 뒤 목록이나 사전을 수정하는 지점도 함께 확인합니다. default_factory로 바꾼 뒤에는 각 객체가 독립된 상태를 가져야 하는지, 함수에서는 호출자가 넘긴 목록을 의도대로 보존해야 하는지 작은 입력으로 확인하면 됩니다. 공유가 필요하다면 그 이유를 주석이나 이름으로 드러내고 ClassVar의 범위를 좁게 유지하는 편이 좋습니다.
다음으로는 dataclass 필드가 실제 API 요청·응답 구조에 들어가는지, 또는 단순한 작업 상태인지 구분해 보십시오. 외부 데이터 검증이 필요한 모델이라면 dataclass의 기본값 문제와 별개로 입력 검증 규칙을 따로 설계해야 합니다.
출처: Python dataclasses 공식 문서, Python typing 공식 문서, Python Programming FAQ