References
- Fluent Python
Contents
- collections.namedtuple
- typing.NamedTuple
- @dataclasses.dataclass
- Type Hints
파이썬에서 기능이 거의 없거나 하나도 없는, 필드들의 컬렉션인 간단한 클래스를 구축하는 몇 가지 방법을 제공합니다. 이 패턴은 'data class'라고 알려져있고, dataclasses는 이 패턴을 지원하는 패키지 중 하나입니다. 이번 포스팅에서는 데이터 클래스를 작성하는 3가지 다른 클래스 빌더를 알아보겠습니다.
- collections.namedtuple : 가장 간단한 방법 (available since Python 2.6)
- typing.NamedTuple : 필드에 타입 힌트(type hint)를 요구하는 다른 방법 (since Python 3.5, with class syntax added in Python 3.6)
- @dataclasses.dataclass : 위의 두 방법보다 더 많은 커스터마이즈를 할 수 있는 클래스 데코레이터. 많은 옵션들을 추가할 수 있음(since Python 3.7)
Overview of data class builders
다음과 같이 지리 좌표계 쌍을 표현하는 간단한 클래스를 살펴보겠습니다.
class Coordinate:
def __init__(self, lat, lon):
self.lat = lat
self.lon = lon
Coordinate 클래스는 위도와 경도를 나타내는 속성을 가지고 있는 역할을 합니다. 이렇게 작성된 __init__은 만약 클래스가 2가지 이상의 속성을 가지게 된다면, 수정할 필요가 생깁니다. 그리고 파이썬 객체로부터 기대되는 기본 기능들을 제공하지도 않습니다.
이번 포스팅에서 살펴볼 데이터 클래스 빌더는 필수적인 __init__, __repr__, __eq__ 메소드를 자동으로 제공하며, 다른 기능들도 제공합니다.
아래 코드는 namedtuple로 구축된 Coordinate 클래스입니다. 이 팩토리 함수는 tuple의 서브클래스를 구축하며 생성된 빌더는 사용자가 지정한 이름과 필드를 가지고 있습니다.
typing.NamedTuple은 동일한 기능을 제공하는데, 각 필드에 타입 어노테이션이 추가됩니다.
NamedTuple은 각 필드를 키워드 인수로 전달해 생성할 수 있습니다.
Coordinate = typing.NamedTuple('Coordinate', lat=float, lon=float)
파이썬 3.6부터 PEP526에 설명하는 것처럼 타입 어노테이션과 함께 typing.NamedTuple은 class 구문에 사용될 수 있습니다. 이렇게 작성하면 더 읽기 쉽고, 메소드를 오버라이드하거나 새로운 것을 추가하기 쉽습니다.
다음 코드는 위에서 작성한 것과 동일한 Coordinate 클래스를 정의합니다. 한 쌍의 float 속성을 가지고 있으며, 좌표계를 커스터마이즈하여 표현하기 위해 커스텀 __str__이 정의됩니다.
from typing import NamedTuple
class Coordinate(NamedTuple):
lat: float
lon: float
def __str__(self):
ns = 'N' if self.lat >= 0 else 'S'
we = 'E' if self.lon >= 0 else 'W'
return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'
NamedTuple이 class구문에서 수퍼클래스처럼 보이지만, 실제로는 아닙니다. typing.NamedTuple은 metaclass의 고급 기능을 사용하여 유저의 클래스 생성을 커스터마이즈합니다. 이는 다음의 코드로 확인할 수 있습니다.
issubclass(Coordinate, typing.NamedTuple) # False issubclass(Coordinate, tuple) # True
typing.NamedTuple에 의해 생성되는 __init__ 메소드에서, 각 필드가 파라미터로 나타나는 순서는 클래스 내에서 각 필드가 선언된 순서와 같습니다.
typing.NamedTuple과 마찬가지로, dataclass 데코레이터는 인스턴스 속성을 선언하기 위해 PEP526 문법을 지원합니다. 데코레이터는 변수의 어노테이션을 읽고 자동으로 클래스의 메소드를 생성합니다. 비교를 위해, dataclass 데코레이터의 도움으로 작성된 Coordinate 클래스는 다음과 같이 작성할 수 있습니다.
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
lat: float
lon: float
def __str__(self):
ns = 'N' if self.lat >= 0 else 'S'
we = 'E' if self.lon >= 0 else 'W'
return f'{abs(self.lat):.1f}°{ns}, {abs(self.lon):.1f}°{we}'
이렇게 작성된 클래스의 바디는 NamedTuple을 사용한 것과 똑같습니다. @dataclass 데코레이터는 상속이나 metaclass에 의존하지 않습니다. dataclass를 사용하여 작성된 클래스는 object의 서브클래스입니다.
Main features
위에서 살펴본 3가지 데이터 클래스 빌더는 많은 것들을 공통으로 가지고 있습니다. 아래 표는 주요 특징들을 정리한 내용입니다.
namedtuple | NamedTuple | dataclass | |
mutable instance | NO | NO | YES |
class statement syntax | NO | YES | YES |
construct dict | x._asdict() | x._asdict() | dataclasses.asdict(x) |
get field names | x._fields | x._fields | [f.name for f in dataclasses.fields(x)] |
get defaults | x._field_defaults | x._field_defaults | [f.default for f in dataclasses.fields(x)] |
get field types | N/A | x.__annotations__ | x.__annotations__ |
new instance with changes | x._replace(...) | x._replace(...) | dataclasses.replace(x, ...) |
new class at runtime | namedtuple(...) | NamedTuple(...) | dataclasses.make_dataclass(...) |
typing.NamedTuple과 @dataclass에 의해 구축된 클래스는 각 필드의 타입 힌트를 갖고 있는 __annotations__ 속성을 가지고 있습니다. 그러나 __annotations__를 직접 읽는 것보다 typing.get_type_hints(my_data_class)를 사용하여 그 정보를 얻는 것이 좋습니다.
그럼 이제 위의 표에서 나온 특징들을 하나씩 살펴보겠습니다.
Mutable Instances
클래스 빌더 간의 주요한 차이점은 collections.namedtuple과 typing.NamedTuple은 tuple 서브클래스를 구축한다는 것입니다. 그러므로 이들의 인스턴스는 불변형입니다. 기본적으로 @dataclass는 가변형 클래스를 만듭니다. 그러나 데코레이터는 키워드 인수 frozen을 받습니다. frozen=True라면, 클래스는 만약 인스턴스가 초기화된 이후에 필드에 값을 할당하려고 시도하면 예외를 발생시킵니다.
Class statement syntax
오직 typing.NamedTuple과 dataclass만 기본 class 문법을 지원하며, 생성하는 클래스에 메소드나 docstring을 쉽게 추가할 수 있도록 해줍니다.
Construct dict
두 named tuple variants는 인스턴스 메소드(._asdict)를 사용해서 데이터 클래스 인스턴스의 필드로부터 dict 객체를 생성할 수 있습니다. dataclasses 모듈은 이와 같은 동작을 수행하는 dataclasses.asdict 함수를 제공합니다.
Get field names and default values
세 가지 클래스 빌더는 빌더에서 구성할 수 있는 필드 이름과 기본값을 얻을 수 있습니다. named tuple 클래스에서 해당 메타데이터는 .fields와 ._fields_defaults 클래스 속성에 있습니다. dataclass로 데코레이트된 클래스에서는 dataclasses 모듈의 fields 함수를 사용하여 동일한 메타데이터를 얻을 수 있습니다. 이들은 name과 default를 포함하여 여러 속성을 가지는 Field 객체의 튜플을 반환합니다.
Get field types
typing.NamedTuple과 @dataclass로 정의된 클래스는 __annotations__ 클래스 속성으로 타입 어노테이션에 대한 필드 이름의 매핑을 가지고 있습니다. 위에서 언급했듯이, 직접 __annotations__를 읽는 것보다 typing.get_type_hints 함수를 사용하는 것이 좋습니다.
New instance with changes
주어진 named tuple 인스턴스 x에서 x._replace(**kwargs)는 주어진 키워드 인수에 따라 대체된 속성 값들을 갖는 새로운 인스턴스를 반환합니다. dataclasses.replace(x, **kwargs) 함수는 이와 동일한 동작을 dataclass로 데코레이트된 클래스의 인스턴스에 대해 수행합니다.
New class at runtime
비록 class 문법이 읽기 쉽더라도, 코딩하기는 어렵습니다. 프레임워크는 런타임에 데이터 클래스를 빌드해야 할 수도 있습니다. 이를 위해 collections.namedtuple의 기본 함수 호출 문법을 사용할 수 있는데, 이는 typing.NamedTuple에서도 지원합니다. dataclasses 모듈은 이와 같은 동작을 위해 make_dataclass 함수를 제공합니다.
Classic Named Tuples
collections.namedtuple 함수는 필드 이름, 클래스 이름, 그리고 __repr__을 가지는 tuple의 서브클래스를 빌드하는 팩토리(factory)입니다. namedtuple로 빌드된 클래스는 튜플이 필요한 어디에서나 사용될 수 있으며, 사실 튜플을 리턴하는 파이썬 표준 라이브러리의 많은 함수들은 편의를 위해 이제 named tuple을 반환합니다.
다음 예제 코드는 도시에 대한 정보를 담고 있는 named tuple을 정의하는 것을 보여줍니다.
tuple의 서브클래스로서, City는 __eq__와 비교 연산을 위한 스페셜 메소드(ex, __lt__)와 같은 유용한 메소드들을 상속받습니다. 따라서 City 인스턴스들의 리스트를 정렬하는 것도 가능합니다.
named tuple는 다음 예제에서 보듯이 _fields 클래스 속성, _make(iterable) 클래스 메소드, _asdict() 인스턴스 메소드와 같은 tuple로부터 상속받은 몇 가지 속성과 메소드를 제공합니다.
_asdict 메소드는 파이썬 3.7까지는 OrderedDict를 반환합니다. 파이썬 3.8부터는 단순히 dict를 반환합니다.
파이썬 3.7부터 namedtuple은 클래스의 가장 오른쪽에 있는 N개의 필드에 대해 N개의 기본값의 iterable을 제공함으로써 defaults 키워드-only 인수를 받습니다. 다음 예제 코드는 reference 필드의 기본값을 가지는 Cooridnate named tuple을 정의하는 방법을 보여줍니다.
Typed Named Tuples
위에서 기본 필드를 가진 Coordinate 클래스는 typing.NamedTuple을 사용하여 다음과 같이 작성할 수 있습니다.
from typing import NamedTuple
class Coordinate(NamedTuple):
lat: float
lon: float
reference: str = 'WGS84'
typing.NamedTuple로 빌드된 클래스는 collections.namedtuple이 생성하는 메소드 외에는 어떠한 메소드도 없으며 tuple로부터 상속받은 메소드도 없습니다. 유일한 차이점은 Python이 런타임에 완전히 무시하는 __annotations__ 클래스 속성의 존재입니다.
typing.NamedTuple의 주요 특징이 타입 어노테이션이기 때문에 먼저 타입 힌트에 대해서 아주 간단히 알아보겠습니다.
Type Hints
Type Hints(a.k.a. type annotations)는 함수 인수, 리턴 값, 변수, 속성에 예상되는 타입을 선언하는 방법입니다. 타입 힌트에 대해 알아야할 첫 번째 사실은 타입 힌트가 파이썬 바이트코드와 인터프리터에 의해 강제되지 않는다는 것입니다.
No runtime effect
파이썬의 타입 힌트를 이해하기 가장 좋은 방법은 'IDE 및 type checker로 확인할 수 있는 documentation'이라고 생각하는 것입니다. 타입 힌트는 파이썬 프로그램의 런타임 동작에 영향을 미치지 않기 때문입니다.
다음 예제 코드를 살펴보겠습니다.
위에서 보듯이 타입 체크가 런타임에 일어나지 않는 것을 확인할 수 있습니다.
위 코드를 파이썬 모듈로 실행해도 어떠한 에러와 경고도 없습니다.
타입 힌트는 주로 Mypy 또는 PyCharm ID의 built-in type checker와 같은 서드파티의 type checkers를 지원하기 위한 것입니다. 이들은 정적 분석 도구이며, 실행 중이 아닌 파이썬의 소스 코드를 체크합니다.
타입 힌트의 효과를 살펴보기 위해, 예를 들어, Mypy를 사용해서 위 코드를 실행시키면 다음과 같은 에러가 발생합니다.
Variable annotation syntax
typing.NamedTuple과 @dataclass는 모두 PEP 526에 정의된 variable annotation 문법을 사용합니다. 이는 class문에서 속성을 정의하는 문맥에서 해당 구문에 대한 간략한 소개입니다.
변수 어노테이션의 기본 문법은 다음과 같습니다.
var_name: some_type
또한, 초기화도 가능합니다. typeing.NamedTuple 또는 @classdata 선언에서 이 값은 생성자를 호출할 때 대응되는 인수를 생략하면 해당 속성의 기본값이 됩니다.
var_name: some_type = a_value
The meaning of variable annotations
위에서 살펴봤듯이 타입 힌트는 런타임에 아무런 영향도 미치지 못합니다. 그러나 import time(모듈이 로드될 때)에서 파이썬은 typing.NamedTuple과 @dataclass에서 사용하는 __annotations__ 딕셔너리를 빌드하기 위해 타입 힌트를 읽습니다.
이를 확인하기 위해서 먼저 간단한 클래스를 정의하여 모듈로 import 해보겠습니다.
# demo_plain.py
class DemoPlainClass:
a: int
b: float = 1.1
c = 'spam'
위 모듈을 import하여 a, b, c 속성을 확인하려고 시도한 결과입니다.
__annotations__ 스페셜 속성은 인터프리터에 의해 생성되어 소스 코드에서 나타난 타입 힌트를 기록합니다.
a는 오직 어노테이션만 되어 있습니다. a에는 어떠한 값도 바인딩되어 있지 않기 때문에 클래스 속성이 되지 않았습니다. b와 c는 클래스 속성으로 저장되는데, 값이 바인딩되어 있기 때문입니다.
Inspecting a typing.NamedTuple
이제 위에서 정의한 DemoPlainClass와 동일한 속성과 어노테이션을 갖고 typing.NamedTuple로 빌드된 클래스를 살펴보겠습니다.
# demo_nt.py
import typing
class DemoNTClass(typing.NamedTuple):
a: int
b: float = 1.1
c = 'spam'
결과는 위와 같습니다.
DemoPlainDemo와는 달리 typing.NamedTuple은 a와 b 클래스 속성을 생성합니다. c 속성은 값이 'spam'인 클래스 속성입니다.
a와 b 클래스 속성은 descriptor이며, 이에 대한 내용은 생략하도록 하겠습니다. 지금은 프로퍼티의 getter와 유사하고, 명시적인 호출 연산자 ()가 필요없는 메소드라고 생각하면 됩니다. 실제로 이는 a와 b가 read-only 인스턴스 속성으로 동작한다는 것을 의미합니다. (튜플은 불변형)
DemoNTClass는 또한 커스텀 docstring도 제공합니다.
이제 DemoNTClass의 인스턴스를 살펴보겠습니다.
nt를 생성하기 위해서, 적어도 DemoNTClass의 a 인수는 제공해야 합니다. 또한, 이 생성자는 b 인수를 받을 수 있는데, 기본값이 1.1로 설정되어 있기 때문에 b는 옵션입니다. nt 객체는 예상한 대로 a와 b 속성을 가지고 있지만, c 속성은 가지고 있지 않으면 파이썬은 이를 클래스로부터 가지고 옵니다.
만약 nt.a, nt.b, nt.c 또는 nt.z에 값을 할당하려고 시도하면, AttributeError 예외가 발생하는데, 에러 메세지가 미묘하게 다릅니다.
Inspecting a class decorated with dataclass
이번에는 dataclass를 사용한 클래스를 살펴보겠습니다.
# demo_dc.py
from dataclasses import dataclass
@dataclass
class DemoDataClass:
a: int
b: float = 1.1
c = 'spam'
이렇게 작성한 모듈을 사용해 __annotations__, __doc__, a/b/c 속성을 살펴보겠습니다.
__annotations__와 __doc__는 예상한 대로 입니다. 하지만, DemoNTClass와는 달리 DemoDataClass에는 a로 명명된 속성이 없습니다. 이는 a 속성은 오직 DemoDataClass의 인스턴스에만 존재하기 때문입니다. 만약 클래스가 frozen이 아니라면, a 속성은 get/set이 가능한 public 속성이 됩니다. 하지만 b와 c는 클래스 속성으로 존재합니다.
다음은 DemoDataClass 인스턴스를 살펴본 결과입니다.
a와 b는 인스턴스 속성이며, c는 인스턴스를 통해 얻은 클래스 속성입니다.
이전에 언급했듯이 DemoDataClass 인스턴스는 가변형이며 런타임에 어떠한 타입 체크도 없습니다.
여기서 이제 dc는 c 속성을 가지게 됩니다. 그렇지만 클래스 속성인 c는 변경되지 않습니다. 그리고 z 속성을 새롭게 추가할 수 있습니다.
More about @dataclass
지금까지 @dataclass를 사용하는 간단한 예제를 살펴봤습니다. 이 데코레이터는 여러 키워드 인수를 받는데, 이 데코레이터의 시그니처(signature)는 다음과 같습니다.
@dataclass(*, init=True, repr=True, eq=True, order=False,
unsafe_hash=False, frozen=False)
첫 번째 위치의 *은 나머지 파라미터가 keyword-only라는 것을 의미합니다.
아래 표는 인수들에 대한 정보를 정리한 것입니다.
option | meaning | default |
init | generate __init__ | True |
repr | generate __repr__ | True |
eq | generate __eq__ | True |
order | generate __lt__, __le__, __gt__, __ge__ | False |
unsafe_hash | generate __hash__ | False |
frozen | 인스턴스를 immutable로 만듦 | False |
일반적으로 기본값을 사용하는 것이 가장 유용합니다. 여기서 변경하여 사용될 가능성이 높은 옵션은 다음의 2가지 옵션입니다.
- frozen=True: 우연히 클래스 인스턴스를 변경하는 것을 방지
- order=True: 데이터 클래스의 인스턴스의 정렬을 가능하도록 함
하지만 Python 객체의 동적인 특성을 고려할 때, frozen=True에 의해 제공되는 보호를 우회하는 것이 그리 어렵지는 않습니다.
만약 eq와 frozen 인수가 둘다 True라면, @dataclass는 적합한 __hash__ 메소드를 생성합니다. 그래서 인스턴스는 hashable해집니다. 생성된 __hash__는 아래에서 살펴볼 필드 옵션(Field Option)을 이용하여 개별적으로 제거되지 않은 모든 필드의 데이터를 사용합니다. 만약 frozen=False(default)라면, @dataclass는 __hash__를 None으로 설정하며 인스턴스가 unhashable하다고 신호를 보내므로 수퍼클래스의 __hash__를 오버라이딩(None으로)합니다.
PEP 557에는 unsafe_hash를 다음과 같이 설명합니다.
비록 권장되지는 않지만, unsafe_hash=True를 통해 데이터 클래스가 __hash__ 메소드를 만들도록 강제할 수 있습니다. 이는 생성한 클래스가 논리적으로는 immutable하지만, 실제로는 수정될 수 있는 경우가 될 수 있습니다. 이는 특별한 케이스며 주의깊게 사용해야 합니다. 이러한 옵션들을 조금 더 자세히 살펴보려면 공식문서(link)를 참조하시길 바랍니다.
Field Options
이미 대부분 기본 옵션들을 살펴봤습니다. 선언한 인스턴스 필드는 생성된 __init__에서 파라미터가 됩니다. 파이썬은 기본값이 없는 파라미터를 기본값이 있는 파라미터 뒤에 위치하는 것을 허용하지 않습니다. 그러므로 기본값이 있는 필드를 선언한 뒤에 있는 나머지 필드는 반드시 기본값을 가져야 합니다.
변경 가능한 기본값(ex, list)은 초보 개발자에게 일반적인 버그 소스입니다. 함수 정의에서 변경 가능한 기본값을 사용하면 문제가 발생할 수 있습니다. 클래스 속성은 종종 데이터 클래스를 포함한 인스턴스에서 기본 속성 값으로 사용됩니다. 그리고 @dataclass는 타입 힌트에서 기본 값을 __init__의 기본값을 가진 파라미터를 만들 때 사용합니다. 버그를 막기 위해서 @dataclass는 아래에서 보여주는 클래스 정의를 거절합니다.
@dataclass
class ClubMember:
name: str
guests: list = []
위의 코드를 실행하면 다음과 같은 에러가 발생합니다. (단, list, dict, set에만 적용됩니다.)
ValueError 메세지는 문제와 해결책을 제시하고 있는데, default_factory를 사용하라고 합니다.
default_factory를 사용하여 위의 문제를 해결한 ClubMember 클래스는 다음과 같습니다.
from dataclasses import dataclass, field
@dataclass
class ClubMember:
name: str
guests: list = field(default_factory=list)
위의 코드에서 guests 필드는 리터럴 리스트 대신 기본값을 dataclasses.field 함수에 default_factory=list를 전달한 호출로 설정합니다.
default_factory 파라미터를 사용하면 데이터 클래스의 인스턴스가 생성될 때마다 기본값을 빌드하기 위해 인수없이 호출되는 함수, 클래스 또는 다른 콜러블을 제공할 수 있습니다. 이렇게 하면 모든 인스턴스가 클래스의 동일한 list를 공유하는 대신 ClubMember의 각 인스턴스가 고유한 list를 갖게 됩니다.
파이썬 3.9부터 적용되는 list[str]와 같은 문법을 사용할 수도 있습니다.
from dataclasses import dataclass, field
@dataclass
class ClubMember:
name: str
# list[str] means "a list of str"
guests: list[str] = field(default_factory=list)
field 함수에는 default_factory외, 아래의 표와 같이 다른 옵션들도 있습니다.
options | meaning | default |
default | default value for field | _MISSING_TYPE (option이 제공되지 않았음을 나타내는 값) |
default_factory | 0-parameter function used to produce a default | _MISSING_TYPE |
init | include field in parameters to __init__ | True |
repr | include field in __repr__ | True |
compare | use field in comparison methods __eq__, __lt__, etc. | True |
hash | include field in __hash__ calculation | None (hash=None은 compare=True일 때만 이 필드가 사용된다는 것을 의미함) |
metadata | mapping with user-defined data; ignored by the @dataclass | None |
기본값이 False인 athlete 필드를 생성하고, 이 필드의 __repr__을 생략하려면 다음과 같이 작성할 수 있습니다.
@dataclass
class ClubMember:
name: str
guests: list[str] = field(default_factory=list)
athlete: bool = field(default=False, repr=False)
Post-init processing
@dataclass에 의해 생성되는 __init__ 메소드는 오직 전달된 인수를 받고, 이 값들을 인스턴스 속성에 할당합니다. 하지만 인스턴스를 초기화하는 것보다 더 많은 것들이 필요할 수 있습니다. 이런 경우에, __post_init__ 메소드를 추가해주면 됩니다. 이 메소드가 존재하면, @dataclass는 __post_init__을 호출하는 코드를 생성된 __init__메소드의 마지막 단계에 추가합니다.
__post_init__은 주로 검증이나, 다른 필드를 기반으로 필드 값을 계산할 때 사용됩니다.
예제 코드를 통해서 살펴보도록 하겠습니다. 먼저 위에서 정의한 ClubMember의 서브클래스인 HackerClubMember에 기대하는 동작은 다음과 같습니다.
"""
``HackerClubMember`` objects accept an optional ``handle`` argument::
>>> anna = HackerClubMember('Anna Ravenscroft', handle='AnnaRaven')
>>> anna
HackerClubMember(name='Anna Ravenscroft', guests=[], handle='AnnaRaven')
If ``handle`` is omitted, it's set to the first part of the member's name::
>>> leo = HackerClubMember('Leo Rochael')
>>> leo
HackerClubMember(name='Leo Rochael', guests=[], handle='Leo')
Members must have a unique handle. The following ``leo2`` will not be created,
because its ``handle`` would be 'Leo', which was taken by ``leo``::
>>> leo2 = HackerClubMember('Leo DaVinci')
Traceback (most recent call last):
...
ValueError: handle 'Leo' already exists.
To fix, ``leo2`` must be created with an explicit ``handle``::
>>> leo2 = HackerClubMember('Leo DaVinci', handle='Neo')
>>> leo2
HackerClubMember(name='Leo DaVinci', guests=[], handle='Neo')
"""
여기서 우리는 키워드 인수로 handle을 반드시 제공해야 하는데, HackerClubMember는 ClubMember로부터 name과 guests를 상속받고, handle 필드를 추가하기 때문입니다. HackerClubMember에서 생성된 docstring은 생성자 호출에서 필드 순서를 보여줍니다.
여기서 <factory>는 어떤 콜러블이 guests의 기본값을 생성한다고 말해주는 것입니다. 여기서 factory는 list 클래스가 될 것 입니다.
dataclasses 모듈 문서의 Inheritance 섹션에는 여러 단계의 상속이 있을 때, 필드들의 순서가 어떻게 계산되는지 설명해주고 있습니다.
HackerClubMember의 구현은 다음과 같습니다.
@dataclass
class HackerClubMember(ClubMember):
all_handles = set() # class attributes
handle: str = '' # instance field of type str with empty string as default value
def __post_init__(self):
cls = self.__class__ # get the class of instance
if self.handle == '':
self.handle = self.name.split()[0]
if self.handle in cls.all_handles:
msg = f'handle {self.handle!r} already exists.'
raise ValueError(msg)
cls.all_handles.add(self.handle)
Initialization variables that are not fields
때때로 인스턴스 필드가 아닌데, __init__으로 전달해야하는 인수가 필요할 수 있습니다. 이러한 인수들은 dataclasses 문서에서 init-only variables라고 불립니다. 이러한 인수들은 선언하기 위해서, dataclasses 모듈은 InitVar이라는 pseudo 타입을 제공합니다. 이는 typing.ClassVar과 동일한 문법을 사용합니다. 문서에서 제공되는 예제는 다음과 같습니다.
@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)
@dataclass Example
from dataclasses import dataclass, field
from typing import Optional
from enum import Enum, auto
from datetime import date
# This Enum will provide type-safe values for the Resource.type field
class ResourceType(Enum):
BOOK = auto()
EBOOK = auto()
VIDEO = auto()
@dataclass
class Resource:
"""Media resource description."""
identifier: str # This is the only required field
title: str = '<untitled>'
creators: list = field(default_factory=list)
date: Optional[date] = None # The value of date can be a datetime.data instance or None
type: ResourceType = ResourceType.BOOK # The field default is ResourceType.BOOK
description: str = ''
language: str = ''
subjects: list = field(default_factory=list)
위의 코드는 @dataclass를 사용하는 한 예제입니다. 이렇게 작성된 dataclass는 다음과 같이 사용할 수 있습니다.
@dataclass로부터 생성된 __repr__도 정상적인 것 같습니다.
하지만 다음과 같이 더 읽기 쉽게 만들 수 있습니다.
__repr__을 구현해서 이 포맷을 사용할 수 있습니다.
from dataclasses import fields
def __repr__(self):
cls = self.__class__
cls_name = cls.__name__
indent = ' ' * 4
res = [f'{cls_name}(']
for f in fields(cls):
value = getattr(self, f.name)
res.append(f'{indent}{f.name} = {value!r},')
res.append(')')
return '\n'.join(res)
'프로그래밍 > Python' 카테고리의 다른 글
[Python] Interfaces, Protocols, and ABCs (2) | 2022.03.23 |
---|---|
[Python] Special Methods for Sequences (0) | 2022.03.21 |
[Python] A Pythonic Object (0) | 2022.03.19 |
[Python] 데코레이터와 클로저 (0) | 2022.03.18 |
[Python] 일급 함수 (Functions as First-Class Objects) (0) | 2022.03.17 |
댓글