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
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
검토자를 위한 안내
이 PEP와 초기 구현은 별도의 저장소에서 작성되었습니다: https://github.com/ericvsmith/dataclasses. 공개 포럼에 의견을 남기기 전에 최소한 이 PEP 끝에 나열된 Discussion을 읽어 주십시오.
초록
이 PEP는 데이터 클래스라는 표준 라이브러리 추가 기능을 설명합니다. 데이터 클래스는 매우 다른 메커니즘을 사용하지만, “기본값이 있는 변경 가능한 namedtuple”로 생각할 수 있습니다. 데이터 클래스는 일반적인 클래스 정의 문법을 사용하므로, 상속, 메타클래스, 독스트링, 사용자 정의 메서드, 클래스 팩토리를 비롯한 Python 클래스의 다른 기능을 자유롭게 사용할 수 있습니다.
PEP 526의 “변수 어노테이션을 위한 문법(Syntax for Variable Annotations)”에서 정의한 타입 어노테이션이 붙은 변수를 찾아 클래스 정의를 검사하는 클래스 데코레이터가 제공됩니다. 이 문서에서는 이러한 변수를 필드라고 부릅니다. 데코레이터는 이 필드들을 사용하여 인스턴스 초기화, 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는 왜 필요합니까?
PEP 526이 추가되면서 Python에는 클래스 멤버의 타입을 지정하는 간결한 방법이 생겼습니다. 이 PEP는 그 문법을 활용하여 데이터 클래스를 기술하는 간단하고 거슬리지 않는 방법을 제공합니다. 두 가지 예외를 제외하면, 지정된 속성 타입 어노테이션은 데이터 클래스에서 완전히 무시됩니다.
데이터 클래스는 어떠한 베이스 클래스나 메타클래스도 사용하지 않습니다. 이러한 클래스의 사용자는 데이터 클래스의 간섭 없이 상속과 메타클래스를 자유롭게 사용할 수 있습니다. 데코레이트된 클래스는 진정으로 “일반적인” Python 클래스입니다. 데이터 클래스 데코레이터는 클래스의 어떠한 사용 방식에도 간섭해서는 안 됩니다.
데이터 클래스의 주요 설계 목표 중 하나는 정적 타입 검사기를 지원하는 것입니다. PEP 526 문법을 사용하는 것이 그 한 예이며, fields() 함수와 @dataclass 데코레이터의 설계 또한 마찬가지입니다. 앞에서 언급한 라이브러리 중 일부는 매우 동적인 특성 때문에 정적 타입 검사기와 함께 사용하기 어렵습니다.
데이터 클래스는 위의 모든 라이브러리를 대체하는 메커니즘이 아니며, 그렇게 의도된 것도 아닙니다. 하지만 표준 라이브러리에 포함되면 더 단순한 사용 사례 중 상당수는 해당 라이브러리 대신 데이터 클래스를 활용할 수 있을 것입니다. 나열된 라이브러리 중 상당수는 서로 다른 기능 집합을 갖추고 있으며, 앞으로도 당연히 계속 존재하고 번성할 것입니다.
데이터 클래스를 사용하기에 적절하지 않은 경우는 언제입니까?
- 튜플이나 딕셔너리와의 API 호환성이 필요한 경우입니다.
- PEP 484와 526이 제공하는 수준을 넘어서는 타입 검증이 필요하거나, 값 검증 또는 변환이 필요한 경우입니다.
명세
이 PEP에서 설명하는 모든 함수는 dataclasses라는 이름의 모듈에 들어갑니다.
클래스를 후처리하여 아래에서 설명하는 생성된 메서드를 추가하는 함수 dataclass가 제공되며, 이 함수는 보통 클래스 데코레이터로 사용됩니다.
dataclass 데코레이터는 클래스를 검사하여 field들을 찾습니다. field는 __annotations__에서 식별되는 모든 변수로 정의됩니다. 즉, 타입 어노테이션이 있는 변수입니다. 아래에서 설명하는 두 가지 예외를 제외하면, 데이터 클래스의 어떤 메커니즘도 어노테이션에 지정된 타입을 검사하지 않습니다.
__annotations__는 클래스 선언 순서를 따르는 순서 있는 매핑임이 보장된다는 점에 유의하십시오. 생성된 모든 메서드에서 필드의 순서는 필드가 클래스에 나타나는 순서와 같습니다.
dataclass 데코레이터는 아래에서 설명하는 다양한 “던더(dunder)” 메서드를 클래스에 추가합니다. 추가되는 메서드 중 하나라도 클래스에 이미 존재하면 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: 참이면(기본값은 False)__lt__,__le__,__gt__,__ge__메서드가 생성됩니다. 이 메서드들은 클래스를 그 필드들을 순서대로 담은 튜플처럼 비교합니다. 비교하는 두 인스턴스는 정확히 같은 타입이어야 합니다.order가 참이고eq가 거짓이면ValueError가 발생합니다.클래스가 이미
__lt__,__le__,__gt__,__ge__중 하나라도 정의하고 있으면ValueError가 발생합니다.unsafe_hash:False이면(기본값)__hash__메서드는eq와frozen의 설정에 따라 생성됩니다.eq와frozen이 모두 참이면 데이터 클래스가__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: 참이면(기본값은 False) 필드에 값을 대입할 때 예외가 발생합니다. 이는 읽기 전용인 동결(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'
이 예제에서 추가되는 __init__ 메서드에는 a와 b가 모두 포함되며, 이 메서드는 다음과 같이 정의됩니다:
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 값은 default와 default_factory 매개변수가 제공되었는지 감지하는 데 사용되는 센티널 객체입니다. None이 default에 유효한 값이기 때문에 이 센티널을 사용합니다.
field()의 매개변수는 다음과 같습니다:
default: 제공되면 이 필드의 기본값이 됩니다.field호출 자체가 기본값이 일반적으로 놓이는 위치를 대신 차지하기 때문에 이 매개변수가 필요합니다.default_factory: 제공되는 경우, 이 필드에 기본값이 필요할 때 호출되는 인자 없는 호출 가능 객체여야 합니다. 여러 용도 중에서도, 아래에서 논의하듯이 가변 기본값을 갖는 필드를 지정하는 데 사용할 수 있습니다.default와default_factory를 모두 지정하는 것은 오류입니다.init: 참(기본값)이면, 이 필드는 생성된__init__메서드의 매개변수로 포함됩니다.repr: 참(기본값)이면, 이 필드는 생성된__repr__메서드가 반환하는 문자열에 포함됩니다.compare: True(기본값)이면, 이 필드는 생성된 동등성 및 비교 메서드(__eq__,__gt__등)에 포함됩니다.hash: bool 또는None일 수 있습니다. True이면, 이 필드는 생성된__hash__메서드에 포함됩니다.None(기본값)이면compare의 값을 사용합니다. 일반적으로 이것이 기대되는 동작입니다. 필드가 비교에 사용된다면 해시에서도 고려되어야 합니다. 이 값을None이외의 값으로 설정하는 것은 권장하지 않습니다.compare=True이면서hash=False로 설정할 만한 한 가지 이유는, 어떤 필드의 해시값을 계산하는 비용이 크고, 그 필드가 동등성 검사에는 필요하며, 타입의 해시값에 기여하는 다른 필드가 있는 경우입니다. 필드가 해시에서 제외되더라도 비교에는 여전히 사용됩니다.metadata: 매핑 또는 None일 수 있습니다. None은 빈 dict로 취급됩니다. 이 값은 읽기 전용으로 만들기 위해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.z는 10이 되고, 클래스 속성 C.t는 20이 되며, 클래스 속성 C.x와 C.y는 설정되지 않습니다.
Field 객체
Field 객체는 정의된 각 필드를 기술합니다. 이 객체들은 내부적으로 생성되며, 모듈 수준 메서드인 fields()가 반환합니다(아래 참조). 사용자는 Field 객체를 절대 직접 인스턴스화해서는 안 됩니다. 문서화된 속성은 다음과 같습니다:
name: 필드의 이름입니다.type: 필드의 타입입니다.default,default_factory,init,repr,hash,compare,metadata는field()선언에서와 동일한 의미와 값을 가집니다.
다른 속성이 존재할 수도 있지만, 이들은 비공개이므로 검사하거나 의존해서는 안 됩니다.
초기화 후 처리
생성된 __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 전용 변수 절을 참조하십시오. 또한 replace()가 init=False 필드를 처리하는 방식에 관한 경고도 참조하십시오.
클래스 변수
dataclass가 실제로 필드의 타입을 검사하는 한 가지 경우는 필드가 PEP 526에 정의된 클래스 변수인지 판단할 때입니다. 이는 필드의 타입이 typing.ClassVar인지 확인하는 방식으로 이루어집니다. 필드가 ClassVar인 경우 필드로 고려되는 대상에서 제외되며 데이터 클래스 메커니즘에서 무시됩니다. 자세한 논의는 [8]을 참조하십시오. 이러한 ClassVar 유사 필드는 모듈 수준 함수 fields()가 반환하지 않습니다.
init 전용 변수
dataclass가 타입 어노테이션을 검사하는 또 다른 경우는 필드가 init 전용 변수인지 판단할 때입니다. 이는 필드의 타입이 dataclasses.InitVar 타입인지 확인하는 방식으로 이루어집니다. 필드가 InitVar인 경우 init 전용 필드라고 하는 유사 필드로 간주됩니다. 진짜 필드가 아니므로 모듈 수준 함수 fields()가 반환하지 않습니다. init 전용 필드는 생성된 __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()는 i와 j에 대해서는 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를 사용한 원래 예제와 같은 문제가 있습니다. 즉, 클래스 인스턴스를 생성할 때 x의 값을 지정하지 않은 클래스 D의 두 인스턴스는 x의 같은 사본을 공유합니다. 데이터 클래스는 일반적인 Python 클래스 생성 방식을 그대로 사용하므로 이 문제도 똑같이 가지고 있습니다. 데이터 클래스가 이 상황을 감지할 수 있는 일반적인 방법은 없습니다. 대신, 데이터 클래스는 list, dict, set 타입의 기본 매개변수를 감지하면 TypeError를 발생시킵니다. 이것은 부분적인 해결책이지만, 흔히 발생하는 많은 오류를 막아 줍니다. 자세한 내용은 기각된 아이디어 절의 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): 데이터 클래스instance를 (팩토리 함수dict_factory를 사용하여) 딕셔너리로 변환합니다. 각 데이터 클래스는 필드의 이름:값 쌍으로 이루어진 딕셔너리로 변환됩니다. 데이터 클래스, 딕셔너리, 리스트, 튜플은 재귀적으로 처리됩니다. 예를 들면:@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): 데이터 클래스instance를 (팩토리 함수tuple_factory를 사용하여) 튜플로 변환합니다. 각 데이터 클래스는 필드 값으로 이루어진 튜플로 변환됩니다. 데이터 클래스, 딕셔너리, 리스트, 튜플은 재귀적으로 처리됩니다.앞의 예제에 이어서:
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만 주어지면type으로typing.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를 호출할 때 반드시 지정해야 합니다.changes에init=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()
사용자의 코드를 깨뜨리지 않고는
Time에second필드를 추가할 수 없게 됩니다. - 변경 가능한 인스턴스를 위한 옵션이 없습니다.
- 기본값을 지정할 수 없습니다.
__init__,__repr__등에 어떤 필드를 사용할지 제어할 수 없습니다.- 상속을 통한 필드 결합을 지원할 수 없습니다.
왜 그냥 typing.NamedTuple을 사용하지 않습니까?
정적으로 정의된 필드를 가진 클래스의 경우, typing.NamedTuple은 타입 어노테이션을 사용하여 데이터 클래스와 비슷한 문법을 지원합니다. 이는 namedtuple을 생성하므로, namedtuple의 장점과 일부 단점을 공유합니다. 데이터 클래스는 typing.NamedTuple과 달리 상속을 통한 필드 결합을 지원합니다.
왜 그냥 attrs를 사용하지 않습니까?
- attrs는 표준 라이브러리로 옮겨질 경우 감당할 수 있는 것보다 더 빠르게 변화합니다.
- attrs는 여기서 제안하지 않는 추가 기능들을 지원합니다: 검증기(validator), 변환기(converter), 메타데이터 등. 데이터 클래스는 이러한 기능을 구현하지 않음으로써 단순성을 얻는 절충을 택합니다.
더 많은 논의는 [12]를 참조하십시오.
post-init 매개변수
InitVar가 추가되기 전의 이 PEP 초기 버전에서는 post-init 함수 __post_init__가 어떤 매개변수도 받지 않았습니다.
매개변수화된 초기화를 수행하는 일반적인 방법은 (데이터 클래스에 국한되지 않고) 대체 classmethod 생성자를 제공하는 것입니다. 예를 들면 다음과 같습니다:
@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__에서 마지막으로 호출되므로, (객체를 생성한 직후에 코드를 실행할 수도 있는) classmethod 생성자를 두는 것은 __post_init__ 함수에 매개변수를 전달할 수 있는 것과 기능적으로 동등합니다.
InitVar를 사용하면 이제 __post_init__ 함수가 매개변수를 받을 수 있습니다. 이 매개변수들은 먼저 __init__에 전달되고, __init__은 이를 다시 __post_init__에 전달하며, 그곳에서 사용자 코드가 필요에 따라 사용할 수 있습니다.
대체 classmethod 생성자와 InitVar 의사 필드 사이의 유일한 실질적 차이는 객체 생성 시 필요한, 필드가 아닌 매개변수와 관련된 것입니다. InitVar를 사용하는 경우, __init__과 모듈 수준의 replace() 함수를 사용할 때 InitVar를 항상 지정해야 합니다. 인스턴스를 생성하는 데 context 객체가 필요하지만 필드로 저장되지는 않는 경우를 생각해 보십시오. 대체 classmethod 생성자를 사용하면 context 매개변수는 항상 선택 사항이 되는데, (__init__을 통한 생성을 막지 않는 한) __init__을 거쳐서도 여전히 객체를 생성할 수 있기 때문입니다. 어느 접근 방식이 더 적절한지는 애플리케이션에 따라 다르겠지만, 두 접근 방식 모두 지원됩니다.
InitVar 필드를 사용하는 또 다른 이유는 클래스 작성자가 __init__ 매개변수의 순서를 제어할 수 있다는 점입니다. 기본값이 있는 모든 필드는 기본값이 없는 모든 필드 뒤에 와야 하므로, 이는 기본값을 가진 일반 필드와 InitVar 필드가 있을 때 특히 중요합니다. 이전 설계에서는 모든 init 전용 필드가 일반 필드 뒤에 왔습니다. 이는 어떤 필드든 기본값을 가지면 모든 init 전용 필드도 기본값을 가져야 한다는 것을 의미했습니다.
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__이 반환된 후 init=False 필드를 원본 객체에서 새로 생성된 객체로 복사하도록 명시했습니다. 그러나 이는 __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)
__post_init__이 실행된 후 init=False 필드를 원본 객체에서 대상 객체로 복사한다면, 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에 진정한 영감을 주었으며, 저는 그들이 내린 설계 결정을 존중합니다.
참고 문헌
Copyright
This document has been placed in the public domain.