Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 557 – 데이터 클래스

Author:
Eric V. Smith <eric at trueblade.com>
Status:
Final
Type:
Standards Track
Created:
02-Jun-2017
Python-Version:
3.7
Post-History:
08-Sep-2017, 25-Nov-2017, 30-Nov-2017, 01-Dec-2017, 02-Dec-2017, 06-Jan-2018, 04-Mar-2018
Resolution:
Python-Dev message

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

검토자 알림

이 PEP와 초기 구현은 별도의 저장소에서 작성되었습니다: https://github.com/ericvsmith/dataclasses. 공개 포럼에 의견을 남기기 전에 이 PEP의 마지막에 나열된 Discussion를 최소한 읽어 보십시오.

초록

이 PEP는 데이터 클래스라는 표준 라이브러리의 추가 기능을 설명합니다. 데이터 클래스는 매우 다른 메커니즘을 사용하지만, “기본값이 있는 변경 가능한 namedtuple”로 생각할 수 있습니다. 데이터 클래스는 일반적인 클래스 정의 구문을 사용하므로, 상속, 메타클래스, 독스트링, 사용자 정의 메서드, 클래스 팩터리 및 기타 Python 클래스 기능을 자유롭게 사용할 수 있습니다.

클래스 데코레이터는 PEP 526에 정의된 “변수 어노테이션 구문”에 따른 타입 어노테이션이 있는 변수를 찾도록 클래스 정의를 검사합니다. 이 문서에서는 이러한 변수를 필드라고 합니다. 데코레이터는 이러한 필드를 사용하여 인스턴스 초기화, repr, 비교 메서드 및 Specification 섹션에 설명된 기타 선택적 메서드를 지원하는 생성된 메서드 정의를 클래스에 추가합니다. 이러한 클래스를 데이터 클래스라고 하지만, 실제로 클래스 자체에 특별한 점은 없습니다. 데코레이터가 클래스에 생성된 메서드를 추가하고 전달받은 것과 동일한 클래스를 반환합니다.

예를 들어:

@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

@dataclass 데코레이터는 InventoryItem 클래스에 다음 메서드와 동등한 메서드를 추가합니다.:

def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0) -> None:
    self.name = name
    self.unit_price = unit_price
    self.quantity_on_hand = quantity_on_hand
def __repr__(self):
    return f'InventoryItem(name={self.name!r}, unit_price={self.unit_price!r}, quantity_on_hand={self.quantity_on_hand!r})'
def __eq__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) == (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __ne__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) != (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __lt__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) < (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __le__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) <= (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __gt__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) > (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented
def __ge__(self, other):
    if other.__class__ is self.__class__:
        return (self.name, self.unit_price, self.quantity_on_hand) >= (other.name, other.unit_price, other.quantity_on_hand)
    return NotImplemented

데이터 클래스를 사용하면 이러한 메서드를 직접 작성하고 유지 관리하지 않아도 됩니다.

근거

주로 속성 조회로 접근할 수 있는 값을 저장하기 위해 존재하는 클래스를 정의하려는 시도는 수없이 많았습니다. 몇 가지 예는 다음과 같습니다.

  • 표준 라이브러리의 collections.namedtuple.
  • 표준 라이브러리의 typing.NamedTuple.
  • 널리 사용되는 attrs [1] 프로젝트.
  • collections.namedtuple에서 영감을 받은 변경 가능한 데이터 형식인 George Sakkis의 recordType 레시피 [2].
  • 온라인에서 찾아볼 수 있는 여러 예제 레시피 [3], 패키지 [4] 및 질문 [5]. David Beazley는 PyCon 2013 메타클래스 강연에서 데이터 클래스의 한 형태를 동기를 부여하는 예제로 사용했습니다 [6].

그렇다면 이 PEP가 필요한 이유는 무엇입니까?

Python은 PEP 526이 추가됨에 따라 클래스 멤버의 타입을 간결하게 지정할 수 있습니다. 이 PEP는 해당 구문을 활용하여 데이터 클래스를 간단하고 눈에 거슬리지 않게 설명하는 방법을 제공합니다. 두 가지 예외를 제외하면, 지정된 속성 타입 어노테이션은 데이터 클래스에서 완전히 무시됩니다.

데이터 클래스에서는 베이스 클래스나 메타클래스를 사용하지 않습니다. 이러한 클래스의 사용자는 데이터 클래스의 어떠한 간섭도 받지 않고 상속과 메타클래스를 자유롭게 사용할 수 있습니다. 데코레이터가 적용된 클래스는 실제로 “일반적인” Python 클래스입니다. 데이터 클래스 데코레이터는 클래스의 어떤 사용 방식도 방해하지 않아야 합니다.

데이터 클래스의 주요 설계 목표 중 하나는 정적 타입 검사기를 지원하는 것입니다. 관련 PEP 526 문법을 사용하는 것이 그 한 예이며, fields() 함수와 @dataclass 데코레이터의 설계도 마찬가지입니다. 위에서 언급한 일부 라이브러리는 매우 동적인 특성 때문에 정적 타입 검사기와 함께 사용하기 어렵습니다.

데이터 클래스는 위의 모든 라이브러리를 대체하기 위한 메커니즘이 아니며, 그러한 용도로 고안된 것도 아닙니다. 그러나 표준 라이브러리에 포함되면 더 단순한 많은 사용 사례에서 대신 데이터 클래스를 활용할 수 있습니다. 나열된 라이브러리 중 많은 수가 서로 다른 기능 집합을 제공하며, 물론 앞으로도 계속 존재하고 발전할 것입니다.

데이터 클래스를 사용하는 것이 적절하지 않은 경우는 언제입니까?

  • 튜플 또는 딕셔너리와의 API 호환성이 필요합니다.
  • PEP 484 및 526에서 제공하는 것 이상의 타입 검증이 필요하거나, 값 검증 또는 변환이 필요합니다.

명세

이 PEP에서 설명하는 모든 함수는 dataclasses라는 이름의 모듈에 포함됩니다.

일반적으로 클래스 데코레이터로 사용되는 dataclass 함수가 제공되며, 이 함수는 아래에 설명하는 생성된 메서드를 추가하고 클래스를 후처리합니다.

dataclass 데코레이터는 클래스에서 fields를 찾습니다. field__annotations__에서 식별되는 모든 변수로 정의됩니다. 즉, 타입 어노테이션이 있는 변수입니다. 아래에서 설명하는 두 가지 예외를 제외하면 데이터 클래스 메커니즘은 어노테이션에 지정된 타입을 검사하지 않습니다.

__annotations__는 클래스 선언 순서에 따른 순서가 있는 매핑임이 보장된다는 점에 유의하십시오. 생성된 모든 메서드에서 필드의 순서는 클래스에 나타나는 순서입니다.

dataclass 데코레이터는 아래에 설명하는 다양한 “던더” 메서드를 클래스에 추가합니다. 추가하려는 메서드가 클래스에 이미 존재하면 TypeError가 발생합니다. 데코레이터는 전달받은 것과 동일한 클래스를 반환하며, 새 클래스는 생성되지 않습니다.

dataclass 데코레이터는 일반적으로 매개변수와 괄호 없이 사용합니다. 그러나 다음과 같은 논리적 시그니처도 지원합니다.:

def dataclass(*, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)

dataclass를 매개변수 없는 단순한 데코레이터로 사용하면 이 시그니처에 문서화된 기본값을 사용하는 것처럼 동작합니다. 즉, @dataclass를 사용하는 다음 세 가지 방식은 동일합니다.:

@dataclass
class C:
    ...

@dataclass()
class C:
    ...

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

dataclass의 매개변수는 다음과 같습니다.

  • init: 참이면(기본값) __init__ 메서드가 생성됩니다.
  • repr: 참이면(기본값) __repr__ 메서드가 생성됩니다. 생성된 repr 문자열에는 클래스 이름과 각 필드의 이름 및 repr이 클래스에서 정의된 순서대로 포함됩니다. repr에서 제외되도록 표시된 필드는 포함되지 않습니다. 예: InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10).

    클래스에 이미 __repr__이 정의되어 있으면 이 매개변수는 무시됩니다.

  • eq는 참이면(기본값), __eq__ 메서드가 생성됩니다. 이 메서드는 해당 클래스를 필드들로 구성된 튜플인 것처럼 순서대로 비교합니다. 비교되는 두 인스턴스는 동일한 타입이어야 합니다.

    클래스가 이미 __eq__를 정의한 경우 이 매개변수는 무시됩니다.

  • order는 참이면(기본값은 거짓), __lt__, __le__, __gt__, __ge__ 메서드가 생성됩니다. 이 메서드들은 해당 클래스를 필드들로 구성된 튜플인 것처럼 순서대로 비교합니다. 비교되는 두 인스턴스는 동일한 타입이어야 합니다. order가 참이고 eq가 거짓이면 ValueError가 발생합니다.

    클래스가 이미 __lt__, __le__, __gt__ 또는 __ge__ 중 하나라도 정의한 경우 ValueError가 발생합니다.

  • unsafe_hashFalse이면(기본값), eqfrozen이 설정된 방식에 따라 __hash__ 메서드가 생성됩니다.

    eqfrozen이 모두 참이면 데이터 클래스가 __hash__ 메서드를 생성합니다. eq가 참이고 frozen이 거짓이면 __hash__None으로 설정되어 해시 불가능함을 나타냅니다(실제로 해시 불가능합니다). eq가 거짓이면 __hash__는 변경하지 않고 그대로 두므로 상위 클래스의 __hash__ 메서드가 사용됩니다(상위 클래스가 object이면 ID 기반 해싱으로 대체됩니다).

    권장하지는 않지만 unsafe_hash=True를 사용하여 데이터 클래스가 __hash__ 메서드를 생성하도록 강제할 수 있습니다. 이는 클래스가 논리적으로 불변이지만 그럼에도 변경될 수 있는 경우에 해당할 수 있습니다. 이는 특수한 사용 사례이므로 신중하게 고려해야 합니다.

    클래스에 __hash__가 이미 명시적으로 정의되어 있으면 __hash__를 추가할 때의 동작이 변경됩니다. 명시적으로 정의된 __hash__는 다음과 같은 경우를 의미합니다.

    • __eq__가 클래스에 정의되어 있고 __hash__None이외의 값으로 정의된 경우입니다.
    • __eq__가 클래스에 정의되어 있고 None이 아닌 __hash__가 정의된 경우입니다.
    • __eq__가 클래스에 정의되어 있지 않고 __hash__가 정의된 경우입니다.

    unsafe_hash가 참이고 명시적으로 정의된 __hash__가 있으면 ValueError가 발생합니다.

    unsafe_hash가 거짓이고 명시적으로 정의된 __hash__가 있으면 __hash__는 추가되지 않습니다.

    자세한 내용은 Python 문서 [7]를 참조하십시오.

  • frozen이 참이면(기본값은 거짓), 필드에 값을 할당할 때 예외가 발생합니다. 이는 읽기 전용인 불변 인스턴스를 모방합니다. __getattr____setattr__중 하나라도 클래스에 정의되어 있으면 ValueError가 발생합니다. 아래 설명을 참조하십시오.

field는 일반적인 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가 발생합니다. 이는 하나의 클래스에서 이러한 상황이 발생하는 경우와 클래스 상속의 결과로 발생하는 경우 모두에 해당합니다.

일반적이고 단순한 사용 사례에는 다른 기능이 필요하지 않습니다. 그러나 일부 데이터 클래스 기능에는 필드별 추가 정보가 필요합니다. 이 추가 정보에 대한 필요를 충족하려면 제공된 field()함수를 호출하여 기본 필드 값을 대체할 수 있습니다. field()의 시그니처는 다음과 같습니다.:

def field(*, default=MISSING, default_factory=MISSING, repr=True,
          hash=None, init=True, compare=True, metadata=None)

MISSING 값은 defaultdefault_factory 매개변수가 제공되었는지 감지하는 센티널 객체입니다. 이 센티널은 Nonedefault의 유효한 값이기 때문에 사용됩니다.

field()에 전달하는 매개변수는 다음과 같습니다:

  • default: 제공된 경우 이 필드의 기본값이 됩니다. field 호출 자체가 기본값의 일반적인 위치를 대체하기 때문에 필요합니다.
  • default_factory: 제공된 경우 이 필드에 기본값이 필요할 때 호출되는 인자 없는 호출 가능 객체여야 합니다. 아래에서 설명하듯이, 이는 여러 목적 중에서도 변경 가능한 기본값을 갖는 필드를 지정하는 데 사용할 수 있습니다. defaultdefault_factory 를 모두 지정하면 오류입니다.
  • init: 참인 경우(기본값), 이 필드는 생성된 __init__ 메서드의 매개변수로 포함됩니다.
  • repr: 참인 경우(기본값), 이 필드는 생성된 __repr__ 메서드가 반환하는 문자열에 포함됩니다.
  • compare: True인 경우(기본값), 이 필드는 생성된 동등성 및 비교 메서드(__eq__, __gt__, 등)에 포함됩니다.
  • hash: bool 또는 None일 수 있습니다. True인 경우, 이 필드는 생성된 __hash__ 메서드에 포함됩니다. None인 경우(기본값), compare의 값을 사용하며, 이는 일반적으로 예상되는 동작입니다. 필드는 비교에 사용되는 경우 해시에 포함되어야 합니다. 이 값을 None이외의 값으로 설정하는 것은 권장되지 않습니다.

    hash=False로 설정하고 compare=True로 설정하는 한 가지 가능한 이유는 필드의 해시 값을 계산하는 비용이 많이 들고, 해당 필드가 동등성 테스트에 필요하며, 타입의 해시 값에 기여하는 다른 필드가 있는 경우입니다. 필드가 해시에서 제외되더라도 비교에는 계속 사용됩니다.

  • metadata: 이는 매핑 또는 None일 수 있습니다. None은 빈 딕셔너리로 처리됩니다. 이 값은 읽기 전용으로 만들기 위해 types.MappingProxyType으로 래핑되며, Field 객체에 노출됩니다. 데이터 클래스에서는 전혀 사용되지 않으며, 서드 파티 확장 메커니즘으로 제공됩니다. 여러 서드 파티가 각각 고유한 키를 가질 수 있으며, 이를 메타데이터에서 네임스페이스로 사용할 수 있습니다.

필드의 기본값이 field()호출로 지정되면 해당 필드의 클래스 속성은 지정된 default값으로 대체됩니다. 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는 설정되지 않습니다.

Field 객체

Field객체는 정의된 각 필자를 설명합니다. 이러한 객체는 내부적으로 생성되며, 아래에 설명된 모듈 수준 메서드인 fields()에 의해 반환됩니다. 사용자는 Field객체를 직접 인스턴스화해서는 안 됩니다. 문서화된 속성은 다음과 같습니다:

  • name: 필드의 이름입니다.
  • type: 필드의 타입입니다.
  • default, default_factory, init, repr, hash, compare, metadatafield() 선언에서와 동일한 의미와 값을 가집니다.

다른 속성이 존재할 수도 있지만, 해당 속성은 비공개이므로 검사하거나 이에 의존해서는 안 됩니다.

post-init 처리

생성된 __init__ 코드는 클래스에 정의되어 있는 경우 __post_init__라는 메서드를 호출합니다. 이 메서드는 self.__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

__post_init__()에 매개변수를 전달하는 방법은 아래의 init-only 변수 절을 참조하십시오. 또한 replace()init=False 필드를 처리하는 방식에 관한 경고도 참조하십시오.

클래스 변수

dataclass가 실제로 필드의 타입을 검사하는 한 가지 경우는 해당 필드가 PEP 526에 정의된 클래스 변수인지 판단하는 경우입니다. 이는 필드의 타입이 typing.ClassVar인지 확인하여 수행합니다. 필드가 ClassVar인 경우 필드로 고려되지 않으며 데이터 클래스 메커니즘에서 무시됩니다. 자세한 논의는 [8]을 참조하십시오. 이러한 ClassVar의사 필드는 모듈 수준의 fields() 함수에서 반환되지 않습니다.

Init-only 변수

dataclass가 타입 어노테이션을 검사하는 또 다른 경우는 필드가 init-only 변수인지 판단하는 경우입니다. 이는 필드의 타입이 dataclasses.InitVar 타입인지 확인하여 수행합니다. 필드가 InitVar인 경우 init-only 필드라는 의사 필드로 간주됩니다. 이는 실제 필드가 아니므로 모듈 수준의 fields() 함수에서 반환되지 않습니다. Init-only 필드는 생성된 __init__ 메서드의 매개변수로 추가되며, 선택적인 __post_init__ 메서드에 전달됩니다. 그 외에는 데이터 클래스에서 사용되지 않습니다.

예를 들어, 클래스를 생성할 때 값이 제공되지 않으면 데이터베이스에서 필드를 초기화한다고 가정하십시오.:

@dataclass
class C:
    i: int
    j: int = None
    database: InitVar[DatabaseType] = 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에 대한 객체는 반환하지 않습니다.

동결된 인스턴스

완전히 불변인 Python 객체를 만드는 것은 불가능합니다. 그러나 @dataclass 데코레이터에 frozen=True를 전달하면 불변성을 흉내 낼 수 있습니다. 이 경우 데이터 클래스는 클래스에 __setattr____delattr__메서드를 추가합니다. 이러한 메서드는 호출되면 FrozenInstanceError를 발생시킵니다.

frozen=True를 사용할 때는 성능이 약간 저하됩니다. __init__은 필드를 초기화하는 데 단순 대입을 사용할 수 없으며, object.__setattr__을 사용해야 합니다.

상속

@dataclass 데코레이터가 데이터 클래스를 생성할 때, 클래스의 모든 베이스 클래스를 역 MRO 순서로(즉, object에서 시작하여) 살펴보고, 찾은 각 데이터 클래스에 대해 해당 베이스 클래스의 필드를 순서가 있는 필드 매핑에 추가합니다. 모든 베이스 클래스 필드를 추가한 후에는 자신의 필드를 순서가 있는 매핑에 추가합니다. 생성되는 모든 메서드는 이렇게 결합되고 계산된 순서가 있는 필드 매핑을 사용합니다. 필드는 삽입 순서로 저장되므로 파생 클래스가 베이스 클래스를 재정의합니다. 예를 들어 다음과 같습니다.:

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

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

최종 필드 목록은 순서대로 x, y, z입니다. x의 최종 타입은 클래스 C에 지정된 대로 int입니다.

C의 생성된 __init__메서드는 다음과 같이 표시됩니다.:

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

기본 팩토리 함수

필드에 default_factory를 지정하면 해당 필드의 기본값이 필요할 때 인자 없이 호출됩니다. 예를 들어 리스트의 새 인스턴스를 만들려면 다음과 같이 사용하십시오.:

l: list = field(default_factory=list)

init=False를 사용하여 필드를 __init__에서 제외하고 해당 필드에도 default_factory를 지정하면, 기본 팩토리 함수는 생성된 __init__함수에서 항상 호출됩니다. 필드에 초기값을 제공할 다른 방법이 없기 때문입니다.

변경 가능한 기본값

Python은 기본 멤버 변수 값을 클래스 속성에 저장합니다. 데이터 클래스를 사용하지 않는 다음 예제를 살펴보십시오.:

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

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

예상대로 클래스 C의 두 인스턴스는 동일한 클래스 변수 x를 공유한다는 점에 유의하십시오.

데이터 클래스를 사용하며, 만약 이 코드가 유효하다면:

@dataclass
class D:
    x: List = []
    def add(self, element):
        self.x += element

다음과 비슷한 코드를 생성합니다.:

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

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

이는 원래 C 클래스를 사용한 예제와 동일한 문제가 있습니다. 즉, 클래스 D에 속하는 두 인스턴스가 클래스 인스턴스를 생성할 때 x 값을 지정하지 않으면 동일한 x 복사본을 공유합니다. 데이터 클래스는 일반적인 Python 클래스 생성만 사용하므로 이 문제도 공유합니다. 데이터 클래스가 이 조건을 감지할 수 있는 일반적인 방법은 없습니다. 대신 데이터 클래스는 list, dict 또는 set 유형의 기본 매개변수를 감지하면 TypeError를 발생시킵니다. 이는 부분적인 해결책이지만 많은 일반적인 오류를 방지합니다. 자세한 내용은 Rejected Ideas 섹션의 Automatically support mutable default values에서 확인하십시오.

기본 팩토리 함수를 사용하면 필드의 기본값으로 변경 가능한 유형의 새 인스턴스를 생성할 수 있습니다.:

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

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

모듈 수준 헬퍼 함수

  • fields(class_or_instance): 이 데이터 클래스의 필드를 정의하는 Field 객체의 튜플을 반환합니다. 데이터 클래스 또는 데이터 클래스의 인스턴스를 모두 인자로 받을 수 있습니다. 데이터 클래스 또는 그 인스턴스가 전달되지 않으면 ValueError를 발생시킵니다. ClassVar 또는 InitVar인 의사 필드는 반환하지 않습니다.
  • asdict(instance, *, dict_factory=dict): 팩토리 함수 dict_factory를 사용하여 데이터 클래스 instance를 딕셔너리로 변환합니다. 각 데이터 클래스는 필드가 이름:값 쌍으로 구성된 딕셔너리로 변환됩니다. 데이터 클래스, 딕셔너리, 리스트 및 튜플 내부로 재귀적으로 들어갑니다. 예를 들면 다음과 같습니다.:
    @dataclass
    class Point:
         x: int
         y: int
    
    @dataclass
    class C:
         l: List[Point]
    
    p = Point(10, 20)
    assert asdict(p) == {'x': 10, 'y': 20}
    
    c = C([Point(0, 0), Point(10, 4)])
    assert asdict(c) == {'l': [{'x': 0, 'y': 0}, {'x': 10, 'y': 4}]}
    

    instance가 데이터 클래스 인스턴스가 아니면 TypeError를 발생시킵니다.

  • astuple(*, tuple_factory=tuple): 팩토리 함수 tuple_factory를 사용하여 데이터 클래스 instance를 튜플로 변환합니다. 각 데이터 클래스는 필드 값으로 구성된 튜플로 변환됩니다. 데이터 클래스, 딕셔너리, 리스트 및 튜플 내부로 재귀적으로 들어갑니다.

    이전 예제에 이어서:

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

    instance가 데이터 클래스 인스턴스가 아니면 TypeError를 발생시킵니다.

  • make_dataclass(cls_name, fields, *, bases=(), namespace=None): 이름이 cls_name이고, fields에 정의된 필드를 가지며, bases에 지정된 베이스 클래스를 상속하고, namespace에 지정된 네임스페이스로 초기화된 새 데이터 클래스를 생성합니다. fields는 요소가 name, (name, type) 또는 (name, type, Field) 중 하나인 이터러블입니다. name만 제공되면 typetyping.Any를 사용합니다. 이 함수는 엄밀히 말해 필수는 아닙니다. __annotations__를 포함하는 새 클래스를 생성하는 모든 Python 메커니즘은 해당 클래스에 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
    
  • replace(instance, **changes): instance와 동일한 유형의 새 객체를 생성하고, changes의 값으로 필드를 대체합니다. instance가 데이터 클래스가 아니면 TypeError를 발생시킵니다. changes의 값이 필드를 지정하지 않으면 TypeError를 발생시킵니다.

    새로 반환되는 객체는 데이터 클래스의 __init__ 메서드를 호출하여 생성합니다. 이렇게 하면 __post_init__이 존재하는 경우에도 호출됩니다.

    기본값이 없는 초기화 전용 변수가 있는 경우, __init____post_init__에 전달할 수 있도록 replace를 호출할 때 지정해야 합니다.

    changesinit=False로 정의된 필드가 포함되어 있으면 오류입니다. 이 경우 ValueError가 발생합니다.

    replace()를 호출할 때 init=False 필드가 작동하는 방식에 유의하십시오. 이러한 필드는 원본 객체에서 복사되지 않고, 초기화되는 경우에만 __post_init__()에서 초기화됩니다. init=False 필드는 드물고 신중하게 사용될 것으로 예상합니다. 이를 사용하는 경우 대체 클래스 생성자 또는 인스턴스 복사를 처리하는 사용자 지정 replace() (또는 유사한 이름의) 메서드를 마련하는 것이 현명할 수 있습니다.

  • is_dataclass(class_or_instance): 매개변수가 데이터 클래스이거나 데이터 클래스의 인스턴스이면 True를 반환하고, 그렇지 않으면 False를 반환합니다.

    클래스 자체가 아니라 클래스가 데이터 클래스의 인스턴스인지 알아야 한다면 not isinstance(obj, type)에 대한 추가 검사를 수행하십시오.:

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

논의

python-ideas 논의

이 논의는 python-ideas [9]에서 시작되었으며, 추가 논의를 위해 GitHub 저장소 [10]로 옮겨졌습니다. 이 논의의 일환으로 필드 검색을 수행하는 데 PEP 526 구문을 사용하기로 결정했습니다.

__slots__를 자동으로 설정하는 기능을 지원합니까?

적어도 최초 릴리스에서는 __slots__를 지원하지 않습니다. __slots__는 클래스 생성 시점에 추가해야 합니다. 데이터 클래스 데코레이터는 클래스가 생성된 후 호출되므로, __slots__를 추가하려면 데코레이터가 새 클래스를 생성하고 __slots__를 설정한 다음 이를 반환해야 합니다. 이 동작은 다소 예상 밖이므로, 데이터 클래스의 초기 버전에서는 __slots__를 자동으로 설정하는 기능을 지원하지 않습니다. 다음과 같은 여러 해결 방법이 있습니다.

  • 클래스 정의에 __slots__를 수동으로 추가하십시오.
  • fields()를 사용하여 클래스를 검사하고 __slots__가 설정된 새 클래스를 생성하는 함수(데코레이터로 사용할 수도 있음)를 작성하십시오.

추가 논의는 [11]를 참조하십시오.

그냥 namedtuple을 사용하면 안 됩니까?

  • 모든 namedtuple은 동일한 수의 필드를 가진 다른 namedtuple과 실수로 비교될 수 있습니다. 예를 들면 다음과 같습니다. Point3D(2017, 6, 2) == Date(2017, 6, 2). 데이터 클래스를 사용하면 이는 False를 반환합니다.
  • namedtuple은 튜플과 실수로 비교될 수 있습니다. 예를 들면 다음과 같습니다. Point2D(1, 10) == (1, 10). 데이터 클래스를 사용하면 이는 False를 반환합니다.
  • 인스턴스는 항상 이터러블이므로 필드를 추가하기 어려울 수 있습니다. 라이브러리가 다음과 같이 정의하는 경우:
    Time = namedtuple('Time', ['hour', 'minute'])
    def get_time():
        return Time(12, 0)
    

    그런 다음 사용자가 이 코드를 다음과 같이 사용하면:

    hour, minute = get_time()
    

    사용자의 코드를 깨뜨리지 않고는 Timesecond필드를 추가할 수 없게 됩니다.

  • 변경 가능한 인스턴스에 대한 옵션이 없습니다.
  • 기본값을 지정할 수 없습니다.
  • __init__, __repr__ 등에 사용할 필드를 제어할 수 없습니다.
  • 상속을 통한 필드 결합을 지원할 수 없습니다.

그냥 typing.NamedTuple을 사용하면 안 됩니까?

정적으로 정의된 필드를 사용하는 클래스의 경우, 타입 어노테이션을 사용하여 데이터 클래스와 유사한 구문을 지원합니다. 이렇게 하면 namedtuple이 생성되므로 namedtuples 이점과 일부 단점을 공유합니다. 데이터 클래스는 typing.NamedTuple과 달리 상속을 통한 필드 결합을 지원합니다.

그냥 attrs를 사용하면 안 됩니까?

  • attrs는 표준 라이브러리에 포함하기에는 수용할 수 있는 속도보다 빠르게 발전합니다.
  • attrs는 여기에서 제안하지 않는 추가 기능인 검증기, 변환기, 메타데이터 등을 지원합니다. 데이터 클래스는 이러한 기능을 구현하지 않음으로써 단순성을 확보하는 절충을 선택합니다.

자세한 논의는 [12]를 참조하십시오.

사후 초기화 매개변수

InitVars가 추가되기 전 이 PEP의 초기 버전에서는 사후 초기화 함수 __post_init__가 매개변수를 전혀 받지 않았습니다.

매개변수화된 초기화를 수행하는 일반적인 방법은 데이터 클래스뿐만 아니라 대체 클래스 메서드 생성자를 제공하는 것입니다. 예를 들어:

@dataclass
class C:
    x: int

    @classmethod
    def from_file(cls, filename):
        with open(filename) as fl:
            file_value = int(fl.read())
        return C(file_value)

c = C.from_file('file.txt')

__post_init__ 함수는 생성된 __init__에서 마지막으로 호출되는 항목이므로, 객체를 생성한 직후 코드를 실행할 수도 있는 클래스 메서드 생성자를 두는 것은 __post_init__ 함수에 매개변수를 전달할 수 있는 것과 기능적으로 동일합니다.

InitVars를 사용하면 이제 __post_init__ 함수가 매개변수를 받을 수 있습니다. 이들은 먼저 __init__에 전달되고, 여기서 다시 __post_init__에 전달되어 사용자 코드가 필요에 따라 사용할 수 있습니다.

대체 클래스 메서드 생성자와 InitVar 의사 필드 사이의 유일한 실질적인 차이는 객체 생성 중 필요한 비필드 매개변수와 관련된 것입니다. InitVars를 사용하면 __init__ 및 모듈 수준의 replace() 함수를 사용할 때 InitVars를 항상 지정해야 합니다. 인스턴스를 생성하는 데 context객체가 필요하지만 이를 필드로 저장하지 않는 경우를 생각해 보십시오. 대체 클래스 메서드 생성자를 사용하면 context매개변수는 항상 선택 사항입니다. __init__을 통해 객체를 생성할 수도 있기 때문입니다(객체 생성을 억제하지 않는 경우). 어느 접근 방식이 더 적절한지는 애플리케이션에 따라 다르지만, 두 접근 방식 모두 지원됩니다.

InitVar필드를 사용하는 또 다른 이유는 클래스 작성자가 __init__매개변수의 순서를 제어할 수 있기 때문입니다. 기본값이 있는 모든 필드는 기본값이 없는 모든 필드 뒤에 와야 하므로, 이는 일반 필드와 기본값이 있는 InitVar필드에서 특히 중요합니다. 이전 설계에서는 모든 초기화 전용 필드가 일반 필드 뒤에 왔습니다. 따라서 어떤 필드에든 기본값이 있으면 모든 초기화 전용 필드에도 기본값이 있어야 했습니다.

asdict 및 astuple 함수 이름

모듈 수준 도우미 함수 asdict()astuple()의 이름은 PEP 8을 준수하지 않는다고 볼 수 있으며, 각각 as_dict()as_tuple()이어야 합니다. 그러나 논의 [13]namedtuple._asdict()attr.asdict()의 일관성을 유지하기로 결정되었습니다.

거부된 아이디어

replace()에서 새 객체를 생성한 후 init=False 필드 복사

init=False인 필드는 정의상 __init__에 전달되지 않고, 대신 기본값으로 초기화되거나 __init__에서 기본 팩토리 함수를 호출하거나 __post_init__의 코드로 초기화됩니다.

이 PEP의 이전 버전에서는 init=False인 필드를 __init__이 반환된 후 원본 객체에서 새로 생성된 객체로 복사하도록 지정했지만, 이는 새 객체를 초기화하는 데 __init____post_init__를 사용하는 것과 일관되지 않는 것으로 판단되었습니다. 예를 들어 다음과 같은 경우를 생각해 보십시오.:

@dataclass
class Square:
    length: float
    area: float = field(init=False, default=0.0)

    def __post_init__(self):
        self.area = self.length * self.length

s1 = Square(1.0)
s2 = replace(s1, length=2.0)

init=False인 필드를 __post_init__가 실행된 후 원본 객체에서 대상 객체로 복사한다면, s2는 올바른 Square(length=2.0, area=4.0)가 아니라 Square(length=2.0, area=1.0)가 되었을 것입니다.

가변 기본값 자동 지원

한 가지 제안은 리터럴 리스트 []가 기본값인 경우 각 인스턴스가 새 리스트를 갖도록 기본값을 자동으로 복사하자는 것이었습니다. 이 결정에는 바람직하지 않은 부작용이 있었으므로, 최종적으로 알려진 세 가지 내장 가변 타입인 list, dict, set을 허용하지 않기로 결정했습니다. 이 내용과 다른 선택지에 대한 전체 논의는 [14]를 참조하십시오.

예제

사용자 지정 __init__ 메서드

생성된 __init__ 메서드만으로는 충분하지 않은 경우가 있습니다. 예를 들어, *args**kwargs를 저장할 객체를 만들고 싶다고 가정해 보십시오.:

@dataclass(init=False)
class ArgHolder:
    args: List[Any]
    kwargs: Mapping[Any, Any]

    def __init__(self, *args, **kwargs):
        self.args = args
        self.kwargs = kwargs

a = ArgHolder(1, 2, three=3)

복잡한 예제

이 코드는 비공개 소스 프로젝트에 존재합니다.:

class Application:
    def __init__(self, name, requirements, constraints=None, path='', executable_links=None, executables_dir=()):
        self.name = name
        self.requirements = requirements
        self.constraints = {} if constraints is None else constraints
        self.path = path
        self.executable_links = [] if executable_links is None else executable_links
        self.executables_dir = executables_dir
        self.additional_items = []

    def __repr__(self):
        return f'Application({self.name!r},{self.requirements!r},{self.constraints!r},{self.path!r},{self.executable_links!r},{self.executables_dir!r},{self.additional_items!r})'

다음과 같이 대체할 수 있습니다.:

@dataclass
class Application:
    name: str
    requirements: List[Requirement]
    constraints: Dict[str, str] = field(default_factory=dict)
    path: str = ''
    executable_links: List[str] = field(default_factory=list)
    executable_dir: Tuple[str] = ()
    additional_items: List[str] = field(init=False, default_factory=list)

데이터 클래스 버전은 더 선언적이고 코드가 적으며 typing을 지원하고, 그 밖의 생성된 함수도 포함합니다.

감사의 말

다음 사람들은 이 PEP와 코드의 개발 과정에서 귀중한 의견을 제공했습니다: Ivan Levkivskyi, Guido van Rossum, Hynek Schlawack, Raymond Hettinger, Lisa Roach. 그들의 시간과 전문 지식에 감사드립니다.

특히 attrs프로젝트를 언급해야 합니다. 이 프로젝트는 이 PEP에 진정한 영감을 주었으며, 그들이 내린 설계 결정을 존중합니다.

참고 자료