dataclasses — 데이터 클래스

dataclasses — 데이터 클래스

dataclasses 모듈은 사용자 정의 클래스에 __init__()__repr__() 같은 생성된 특수 메서드를 자동으로 추가하는 데코레이터와 함수를 제공해요. 원래 PEP 557에서 기술됐습니다.

출처: Python 표준 라이브러리

본문

생성된 메서드에 사용할 멤버 변수는 PEP 526 타입 어노테이션으로 정의해요. 예를 들어 이 코드는:

from dataclasses import dataclass

@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

다른 것들 중에서도 이런 __init__()을 추가할 거예요.

def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0):
    self.name = name
    self.unit_price = unit_price
    self.quantity_on_hand = quantity_on_hand

이 메서드는 클래스에 자동으로 추가된다는 점에 주의하세요. 위 InventoryItem 정의에서는 직접 지정되지 않았어요.

모듈 내용

@dataclasses.dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False) — 아래 설명대로 클래스에 생성된 특수 메서드를 추가하는 데코레이터예요.

@dataclass 데코레이터는 클래스를 검사해 **필드(field)**를 찾아요. 필드는 타입 어노테이션이 있는 클래스 변수로 정의됩니다. 아래 설명할 두 가지 예외를 제외하면, @dataclass는 변수 어노테이션에 지정된 타입을 검사하지 않아요.

모든 생성된 메서드에서 필드의 순서는 클래스 정의에 나타난 순서와 같습니다. @dataclass 데코레이터는 다양한 "dunder" 메서드를 클래스에 추가합니다. 추가된 메서드 중 하나가 이미 클래스에 있으면 동작은 매개변수에 따라 달라져요. 데코레이터는 호출받은 같은 클래스를 반환하며, 새 클래스를 만들지 않아요.

@dataclass를 매개변수 없는 단순 데코레이터로 쓰면 이 시그니처에 문서화된 기본값을 가진 것처럼 동작해요. 즉 다음 세 가지 @dataclass 사용은 동등합니다.

@dataclass
class C:
    ...

@dataclass()
class C:
    ...

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False,
           match_args=True, kw_only=False, slots=False, weakref_slot=False)
class C:
    ...

@dataclass의 매개변수들:

  • init — 참(기본)이면 __init__() 메서드가 생성돼요. 클래스가 이미 __init__()을 정의하면 이 매개변수는 무시됩니다.

  • repr — 참(기본)이면 __repr__() 메서드가 생성돼요. 생성된 repr 문자열은 클래스 이름과 각 필드의 이름·repr을 클래스에 정의된 순서대로 가집니다. repr에서 제외된 것으로 표시된 필드는 포함되지 않아요. 예: InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10).

  • eq — 참(기본)이면 __eq__() 메서드가 생성돼요. 이 메서드는 각 필드를 순서대로 비교해 클래스를 비교합니다. 비교의 두 인스턴스는 동일한 타입이어야 해요. 클래스가 이미 __eq__()을 정의하면 무시됩니다.

  • order — 참(기본은 False)이면 __lt__(), __le__(), __gt__(), __ge__() 메서드가 생성돼요. 이들은 인스턴스를 그 필드들의 튜플인 것처럼 순서대로 비교합니다. 두 인스턴스는 동일한 타입이어야 해요. order가 참이고 eq가 거짓이면 ValueError가 발생하고, 클래스가 이미 __lt__() 등 중 하나를 정의하면 TypeError가 발생합니다.

  • unsafe_hash — 참이면 안전하지 않을 수 있어도 dataclass가 __hash__() 메서드를 만들도록 강제해요. 그 외에는 eqfrozen 설정에 따라 __hash__() 메서드를 생성합니다. 기본값은 False예요. __hash__()는 내장 hash()와, 객체가 딕셔너리·집합 같은 해시 컬렉션에 추가될 때 사용됩니다. __hash__()가 있다는 것은 클래스 인스턴스가 불변이라는 뜻을 함의해요. 기본적으로 @dataclass는 안전할 때만 암시적 __hash__()를 추가하고, 이미 명시적으로 정의된 __hash__()는 추가하거나 바꾸지 않아요. unsafe_hash=True로 강제할 수도 있지만 권장하지 않습니다.

    __hash__()의 암시적 생성 규칙은 다음과 같습니다 (dataclass의 명시적 __hash__()unsafe_hash=True를 동시에 가질 수 없고, 그렇게 하면 TypeError): eqfrozen이 모두 참이면 기본적으로 __hash__()를 생성. eq가 참이고 frozen이 거짓이면 __hash__()None으로 설정해 해시 불가로 표시(변경 가능하므로 실제로 그럼). eq가 거짓이면 __hash__()는 그대로 두어 상위 클래스의 __hash__()를 사용(상위가 object면 id 기반 해싱으로 폴백).

  • frozen — 참(기본 False)이면 필드에 할당하면 예외가 발생해요. 읽기 전용 불변 인스턴스를 모방합니다. 클래스에 __setattr__()이나 __delattr__()이 정의돼 있고 frozen이 참이면 TypeError가 발생해요.

  • match_args — 참(기본 True)이면 생성된 __init__() 메서드의 keyword-only가 아닌 매개변수 목록에서 __match_args__ 튜플이 만들어집니다. 거짓이거나 __match_args__가 이미 정의돼 있으면 생성하지 않아요.

  • kw_only — 참(기본 False)이면 모든 필드가 keyword-only로 표시됩니다. keyword-only 필드는 __init__()을 호출할 때 그 필드에서 생성된 __init__() 매개변수를 키워드로 지정해야 한다는 효과뿐이에요. keyword-only 필드는 __match_args__에 포함되지 않습니다.

  • slots — 참(기본 False)이면 __slots__ 속성이 생성되고 새 클래스가 원래 클래스 대신 반환돼요. __slots__이 이미 정의돼 있으면 TypeError가 발생합니다. slots=True일 때 기본 클래스 __init_subclass__()에 매개변수를 전달하면 TypeError가 발생하니 주의하세요.

  • weakref_slot — 참(기본 False)이면 인스턴스를 weakref 가능하게 만드는 데 필요한 "__weakref__"라는 슬롯을 추가해요. slots=True 없이 weakref_slot=True를 지정하는 것은 오류입니다.

필드는 일반 Python 문법을 사용해 선택적으로 기본값을 지정할 수 있어요.

@dataclass
class C:
    a: int       # 'a' has no default value
    b: int = 0   # assign a default value for 'b'

이 예에서 ab 둘 다 추가된 __init__() 메서드에 포함되며, 다음과 같이 정의됩니다.

def __init__(self, a: int, b: int = 0):

기본값 없는 필드가 기본값 있는 필드 뒤에 오면 TypeError가 발생해요. 이는 단일 클래스에서든 클래스 상속의 결과로든 마찬가지입니다.

dataclasses.field(*, default=MISSING, default_factory=MISSING, init=True, repr=True, hash=None, compare=True, metadata=None, kw_only=MISSING, doc=None) — 흔하고 단순한 사용 사례에서는 다른 기능이 필요 없어요. 하지만 일부 dataclass 기능은 필드별 추가 정보를 요구합니다. 이 추가 정보 요구를 충족하려면 기본 필드 값을 제공된 field() 함수 호출로 바꿀 수 있어요.

@dataclass
class C:
    mylist: list[int] = field(default_factory=list)

c = C()
c.mylist += [1, 2, 3]

위에서 보듯 MISSING 값은 일부 매개변수가 사용자에 의해 제공됐는지 감지하는 데 쓰는 센티널 객체예요. 이 센티널은 일부 매개변수에서 None이 구별되는 의미를 가진 유효한 값이기 때문에 사용합니다. 어떤 코드도 MISSING 값을 직접 쓰면 안 됩니다.

field()의 매개변수:

  • default — 제공되면 이 필드의 기본값이 돼요. field() 호출 자체가 기본값의 일반 위치를 대체하므로 필요합니다.
  • default_factory — 제공되면 이 필드에 기본값이 필요할 때 호출되는 0-인자 호출 가능 객체여야 해요. 변경 가능한 기본값을 가진 필드를 지정하는 데 쓸 수 있어요. defaultdefault_factory를 둘 다 지정하는 것은 오류입니다.
  • init — 참(기본)이면 이 필드는 생성된 __init__() 메서드의 매개변수로 포함됩니다.
  • repr — 참(기본)이면 이 필드는 생성된 __repr__() 메서드가 반환하는 문자열에 포함됩니다.
  • hashbool 또는 None일 수 있어요. 참이면 생성된 __hash__()에 포함, 거짓이면 제외, None(기본)이면 compare 값을 사용해요(비교에 쓰이는 필드는 해시에 포함돼야 하므로 보통 기대 동작). None이 아닌 다른 값으로 설정하는 것은 권장되지 않아요.
  • compare — 참(기본)이면 이 필드는 생성된 동등·비교 메서드(__eq__(), __gt__() 등)에 포함됩니다.
  • metadata — 매핑 또는 None일 수 있어요. None은 빈 dict로 취급됩니다. 이 값은 MappingProxyType()으로 감싸 읽기 전용으로 만들고 Field 객체에 노출돼요. Data Classes는 전혀 사용하지 않으며, 서드파티 확장 메커니즘으로 제공됩니다.
  • kw_only — 참이면 이 필드가 keyword-only로 표시됩니다. 생성된 __init__() 메서드의 매개변수를 계산할 때 사용돼요. keyword-only 필드는 __match_args__에도 포함되지 않습니다.
  • doc — 이 필드의 선택적 docstring.

필드의 기본값이 field() 호출로 지정되면, 이 필드의 클래스 속성은 지정된 기본값으로 대체돼요. default가 제공되지 않으면 클래스 속성은 삭제됩니다. 의도는 @dataclass 데코레이터가 실행된 후 클래스 속성이 모두 기본값을 담게 되는 것입니다(기본값 자체가 지정된 것처럼). 예를 들어:

@dataclass
class C:
    x: int
    y: int = field(repr=False)
    z: int = field(repr=False, default=10)
    t: int = 20

다음 후에 클래스 속성 C.z10, C.t20이 되고, C.xC.y는 설정되지 않습니다.

class dataclasses.FieldField 객체는 각 정의된 필드를 설명해요. 이 객체들은 내부적으로 만들어지고, 모듈 수준의 fields() 메서드(아래 참고)가 반환합니다. 사용자는 Field 객체를 직접 인스턴스화하면 안 됩니다. 문서화된 속성은 name, type, 그리고 field() 함수와 동일한 의미·값을 가진 default, default_factory, init, repr, hash, compare, metadata, kw_only입니다.

class dataclasses.InitVarInitVar[T] 타입 어노테이션은 init-only인 변수를 설명해요. InitVar로 어노테이션된 필드는 pseudo-field로 간주되어, fields() 함수가 반환하지 않고 __init__()과 선택적 __post_init__()에 매개변수로 추가하는 것 외에는 어떤 방식으로도 사용되지 않아요.

dataclasses.fields(class_or_instance) — 이 dataclass의 필드를 정의하는 Field 객체의 튜플을 반환해요. dataclass나 dataclass 인스턴스를 받습니다. dataclass(또는 그 인스턴스)가 아니면 TypeError를 일으켜요. ClassVarInitVar인 pseudo-field를 반환하지는 않습니다.

dataclasses.asdict(obj, *, dict_factory=dict) — dataclass obj를 dict으로 변환해요(dict_factory 팩토리 함수 사용). 각 dataclass는 name: value 쌍의 필드 dict으로 변환됩니다. dataclass·dict·list·tuple은 재귀적으로 들어가고, 다른 객체는 copy.deepcopy()로 복사돼요.

중첩 dataclass에 asdict()를 쓰는 예:

@dataclass
class Point:
     x: int
     y: int

@dataclass
class C:
     mylist: list[Point]

p = Point(10, 20)
assert asdict(p) == {'x': 10, 'y': 20}

c = C([Point(0, 0), Point(10, 4)])
assert asdict(c) == {'mylist': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}

얕은 복사를 만들려면 다음 우회 방법을 쓸 수 있어요.

{field.name: getattr(obj, field.name) for field in fields(obj)}

obj가 dataclass 인스턴스가 아니면 asdict()TypeError를 일으킵니다.

dataclasses.astuple(obj, *, tuple_factory=tuple) — dataclass obj를 튜플로 변환해요(tuple_factory 팩토리 함수 사용). 각 dataclass는 필드 값의 튜플로 변환됩니다. dataclass·dict·list·tuple은 재귀적으로 들어가고, 다른 객체는 copy.deepcopy()로 복사돼요.

assert astuple(p) == (10, 20)
assert astuple(c) == ([(0, 0), (10, 4)],)

얕은 복사를 위한 우회 방법:

tuple(getattr(obj, field.name) for field in dataclasses.fields(obj))

dataclasses.make_dataclass(cls_name, fields, *, bases=(), namespace=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, match_args=True, kw_only=False, slots=False, weakref_slot=False, module=None, decorator=dataclass) — 이름 cls_name, fields에 정의된 필드, bases에 주어진 기본 클래스, namespace로 초기화된 새 dataclass를 만들어요. fields의 각 요소는 name, (name, type), (name, type, Field) 중 하나예요. name만 주어지면 typing.Any를 타입으로 씁니다. init, repr, eq, order, unsafe_hash, frozen, match_args, kw_only, slots, weakref_slot 값은 @dataclass에서와 같은 의미예요.

module이 정의되면 dataclass의 __module__ 속성이 그 값으로 설정됩니다. 기본값은 호출자의 모듈 이름이에요. decorator 매개변수는 dataclass를 만드는 데 쓸 호출 가능 객체로, 첫 인자로 클래스 객체를, @dataclass와 같은 키워드 인자를 받아야 해요. 기본적으로 @dataclass 함수가 사용됩니다.

이 함수는 필수는 아니에요. __annotations__이 있는 새 클래스를 만드는 어떤 Python 메커니즘이든 @dataclass 함수를 적용해 그 클래스를 dataclass로 바꿀 수 있으니까요. 편의를 위해 제공됩니다. 예:

C = make_dataclass('C',
                   [('x', int),
                     'y',
                    ('z', int, field(default=5))],
                   namespace={'add_one': lambda self: self.x + 1})

다음과 동등합니다:

@dataclass
class C:
    x: int
    y: 'typing.Any'
    z: int = 5

    def add_one(self):
        return self.x + 1

dataclasses.replace(obj, /, **changes)obj와 같은 타입의 새 객체를 만들어, 필드를 changes의 값으로 바꿔요. obj가 Data Class가 아니면 TypeError, changes의 키가 주어진 dataclass의 필드 이름이 아니면 TypeError를 일으킵니다. 새로 반환된 객체는 dataclass의 __init__() 메서드를 호출해 만들어지므로, __post_init__()이 있으면 그것도 호출됨을 보장합니다. 기본값이 없는 init-only 변수가 있으면 그것들을 replace() 호출에 지정해 __init__()__post_init__()에 전달할 수 있게 해야 해요. changesinit=False로 정의된 필드를 포함하는 것은 오류이며 ValueError가 발생합니다.

dataclass 인스턴스는 제네릭 함수 copy.replace()도 지원합니다.

dataclasses.is_dataclass(obj) — 매개변수가 dataclass(서브클래스 포함, 제네릭 별칭 제외)이거나 그 인스턴스면 True, 아니면 False를 반환해요. 클래스가 dataclass 자체가 아니라 dataclass의 인스턴스인지 알아야 하면 not isinstance(obj, type) 추가 검사가 필요합니다.

def is_dataclass_instance(obj):
    return is_dataclass(obj) and not isinstance(obj, type)

dataclasses.MISSING — 누락된 기본값이나 default_factory를 나타내는 센티널 값.

dataclasses.KW_ONLY — 타입 어노테이션으로 쓰는 센티널 값. KW_ONLY 타입의 pseudo-field 뒤에 오는 모든 필드는 keyword-only 필드로 표시됩니다. KW_ONLY 타입의 pseudo-field는 그 외에는 완전히 무시돼요(그 필드의 이름 포함). 관례상 _라는 이름이 KW_ONLY 필드에 쓰입니다. keyword-only 필드는 클래스를 인스턴스화할 때 키워드로 지정해야 하는 __init__() 매개변수를 나타내요.

@dataclass
class Point:
    x: float
    _: KW_ONLY
    y: float
    z: float

p = Point(0, y=1.5, z=2.0)

단일 dataclass에서 KW_ONLY 타입 필드를 둘 이상 지정하는 것은 오류입니다.

exception dataclasses.FrozenInstanceErrorfrozen=True로 정의된 dataclass에서 암시적으로 정의된 __setattr__()이나 __delattr__()이 호출될 때 발생해요. AttributeError의 서브클래스입니다.

초기화 후 처리(Post-init)

dataclasses.__post_init__() — 클래스에 정의되면 생성된 __init__()이 보통 self.__post_init__()으로 호출해요. 하지만 InitVar 필드가 정의되어 있으면 클래스에 정의된 순서대로 그것들도 __post_init__()에 전달됩니다. __init__() 메서드가 생성되지 않으면 __post_init__()도 자동 호출되지 않아요.

이를 통해 한 개 이상의 다른 필드에 의존하는 필드 값을 초기화할 수 있어요.

@dataclass
class C:
    a: float
    b: float
    c: float = field(init=False)

    def __post_init__(self):
        self.c = self.a + self.b

@dataclass가 생성한 __init__() 메서드는 기본 클래스의 __init__() 메서드를 호출하지 않아요. 기본 클래스의 __init__()을 호출해야 한다면 보통 __post_init__() 메서드에서 호출합니다.

class Rectangle:
    def __init__(self, height, width):
        self.height = height
        self.width = width

@dataclass
class Square(Rectangle):
    side: float

    def __post_init__(self):
        super().__init__(self.side, self.side)

단 일반적으로 dataclass가 생성한 __init__() 메서드를 호출할 필요는 없어요. 파생 dataclass가 dataclass인 기본 클래스의 모든 필드를 초기화하는 일을 처리해 주니까요.

클래스 변수

@dataclass가 실제로 필드의 타입을 검사하는 드문 곳 중 하나는 필드가 PEP 526에 정의된 클래스 변수인지 결정하는 것입니다. 필드 타입이 typing.ClassVar인지 확인해 이렇게 해요. 필드가 ClassVar이면 필드로 고려에서 제외되고 dataclass 메커니즘이 무시합니다. 이런 ClassVar pseudo-field는 모듈 수준 fields() 함수가 반환하지 않아요.

init-only 변수

@dataclass가 타입 어노테이션을 검사하는 또 다른 곳은 필드가 init-only 변수인지 결정하는 것입니다. 필드 타입이 InitVar 타입인지 보고 이렇게 해요. 필드가 InitVar이면 init-only 필드라는 pseudo-field로 간주됩니다. 진짜 필드가 아니라서 모듈 수준 fields() 함수가 반환하지 않아요. init-only 필드는 생성된 __init__() 메서드에 매개변수로 추가되고 선택적 __post_init__() 메서드로 전달됩니다. 그 외에는 dataclasses가 사용하지 않습니다.

예를 들어 클래스를 만들 때 값이 제공되지 않으면 데이터베이스에서 필드를 초기화한다고 가정해 봅시다.

@dataclass
class C:
    i: int
    j: int | None = None
    database: InitVar[DatabaseType | None] = None

    def __post_init__(self, database):
        if self.j is None and database is not None:
            self.j = database.lookup('j')

c = C(10, database=my_database)

이 경우 fields()ij에 대한 Field 객체를 반환하지만, database에 대한 것은 반환하지 않습니다.

불변 인스턴스(Frozen instances)

진정한 불변 Python 객체를 만드는 것은 불가능해요. 하지만 @dataclass 데코레이터에 frozen=True를 넘기면 불변성을 모방할 수 있습니다. 그 경우 dataclasses는 클래스에 __setattr__()__delattr__() 메서드를 추가하고, 이 메서드들은 호출되면 FrozenInstanceError를 일으켜요.

frozen=True를 쓰면 약간의 성능 페널티가 있어요: __init__()이 필드를 초기화하는 데 단순 할당을 쓸 수 없고 object.__setattr__()을 사용해야 합니다.

상속

@dataclass 데코레이터가 dataclass를 만들 때 클래스의 모든 기본 클래스를 역 MRO(즉 object에서 시작)로 훑으며, 찾은 각 dataclass에 대해 그 기본 클래스의 필드를 필드의 순서 매핑에 추가해요. 모든 기본 클래스 필드를 추가한 뒤 자신의 필드를 순서 매핑에 추가합니다. 생성된 모든 메서드는 이 결합·계산된 순서 매핑을 사용해요. 필드가 삽입 순서이므로 파생 클래스가 기본 클래스를 덮어씁니다.

@dataclass
class Base:
    x: Any = 15.0
    y: int = 0

@dataclass
class C(Base):
    z: int = 10
    x: int = 15

최종 필드 목록은 순서대로 x, y, z예요. x의 최종 타입은 class C에 지정된 대로 int입니다. C에 대해 생성된 __init__() 메서드는 이렇게 보일 거예요.

def __init__(self, x: int = 15, y: int = 0, z: int = 10):

init()에서 keyword-only 매개변수 재정렬

__init__()에 필요한 매개변수를 계산한 뒤, 모든 keyword-only 매개변수는 모든 일반(비-keyword-only) 매개변수 뒤로 이동해요. Python에서 keyword-only 매개변수가 비-keyword-only 뒤에 와야 하기 때문입니다.

이 예에서 Base.y, Base.w, D.t는 keyword-only 필드이고, Base.xD.z는 일반 필드예요.

@dataclass
class Base:
    x: Any = 15.0
    _: KW_ONLY
    y: int = 0
    w: int = 1

@dataclass
class D(Base):
    z: int = 10
    t: int = field(kw_only=True, default=0)

D에 대해 생성된 __init__() 메서드는 이렇게 보일 거예요.

def __init__(self, x: Any = 15.0, z: int = 10, *, y: int = 0, w: int = 1, t: int = 0):

매개변수가 필드 목록에 나타난 순서와 다르게 재정렬됐음을 주의하세요: 일반 필드에서 파생된 매개변수 뒤에 keyword-only 필드에서 파생된 매개변수가 옵니다. 재정렬된 __init__() 매개변수 목록에서 keyword-only 매개변수의 상대적 순서는 유지됩니다.

기본 팩토리 함수

field()default_factory를 지정하면, 이 필드에 기본값이 필요할 때 0개의 인자로 호출돼요. 예를 들어 새 list 인스턴스를 만들려면:

mylist: list = field(default_factory=list)

필드가 __init__()에서 제외되고(init=False 사용) default_factory도 지정하면, 기본 팩토리 함수는 항상 생성된 __init__() 함수에서 호출됩니다. 필드에 초기 값을 줄 다른 방법이 없기 때문이에요.

변경 가능한 기본값

Python은 기본 멤버 변수 값을 클래스 속성에 저장해요. dataclasses를 쓰지 않는 이 예를 보세요.

class C:
    x = []
    def add(self, element):
        self.x.append(element)

o1 = C()
o2 = C()
o1.add(1)
o2.add(2)
assert o1.x == [1, 2]
assert o1.x is o2.x

class C의 두 인스턴스가 예상대로 같은 클래스 변수 x를 공유함을 주의하세요.

dataclasses를 쓰면, 이 코드가 유효하다면:

@dataclass
class D:
    x: list = []      # This code raises ValueError
    def add(self, element):
        self.x.append(element)

이런 코드를 생성했을 거예요.

class D:
    x = []
    def __init__(self, x=x):
        self.x = x
    def add(self, element):
        self.x.append(element)

assert D().x is D().x

class C를 쓰는 원래 예와 같은 문제가 있어요. 즉 x에 값을 지정하지 않고 인스턴스를 만든 class D의 두 인스턴스는 같은 x 복사본을 공유합니다. dataclasses가 그냥 일반 Python 클래스 생성만 사용하기 때문에 이 동작을 공유해요. Data Classes가 이 조건을 감지할 일반적인 방법은 없어요. 대신 @dataclass 데코레이터는 해시 불가한 기본 매개변수를 감지하면 ValueError를 일으킵니다. 값이 해시 불가하면 변경 가능하다는 가정이죠. 이는 부분적인 해결책이지만 많은 흔한 오류를 막아 줍니다.

기본 팩토리 함수를 쓰는 것은 변경 가능 타입의 새 인스턴스를 필드의 기본값으로 만드는 방법이에요.

@dataclass
class D:
    x: list = field(default_factory=list)

assert D().x is not D().x

디스크립터 타입 필드

기본값으로 디스크립터 객체가 할당된 필드는 다음과 같은 특수 동작이 있어요.

  • dataclass의 __init__() 메서드에 전달된 필드 값은 디스크립터 객체를 덮어쓰는 대신 디스크립터의 __set__() 메서드로 전달됩니다.
  • 마찬가지로 필드를 가져오거나 설정할 때도 디스크립터 객체를 반환하거나 덮어쓰는 대신 디스크립터의 __get__()/__set__() 메서드가 호출됩니다.
  • 필드가 기본값을 포함하는지 결정하기 위해 @dataclass는 클래스 접근 형식 descriptor.__get__(obj=None, type=cls)으로 디스크립터의 __get__() 메서드를 호출해요. 이 경우 디스크립터가 값을 반환하면 그 값을 필드의 기본값으로 사용합니다. 반면 디스크립터가 AttributeError를 일으키면 필드에 기본값이 제공되지 않아요.
class IntConversionDescriptor:
    def __init__(self, *, default):
        self._default = default

    def __set_name__(self, owner, name):
        self._name = "_" + name

    def __get__(self, obj, type):
        if obj is None:
            return self._default

        return getattr(obj, self._name, self._default)

    def __set__(self, obj, value):
        setattr(obj, self._name, int(value))

@dataclass
class InventoryItem:
    quantity_on_hand: IntConversionDescriptor = IntConversionDescriptor(default=100)

i = InventoryItem()
print(i.quantity_on_hand)   # 100
i.quantity_on_hand = 2.5    # calls __set__ with 2.5
print(i.quantity_on_hand)   # 2

필드가 디스크립터 타입으로 어노테이션됐지만 그 기본값으로 디스크립터 객체가 할당되지 않았다면, 필드는 일반 필드처럼 동작한다는 점에 주의하세요.

더 알아보기