dataclass
언제 써야해?
- namedtuple의 property가 4-5개보다 더 많아질 떄 사용 고려
- class의 단점인 아래의 것들을 피하고 싶을 떄 사용 고려
- boiler-plate 문제가 있음
- id / name / brithdate / admin 이 반복됨
class User:
def __init__(
self, id: int, name: str, birthdate: date, admin: bool = False
) -> None:
self.id = id
self.name = name
self.birthdate = birthdate
self.admin = admin
__repr__()
메서드를 추가하여 필드 값이 모두 출력되도록 하지 않는 이상, instance를 출력할 때 field 값이 나타나지 않아서 불편함.
>>> user = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user
<__main__.User object at 0x105558100>
>>> user1 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user2 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user1 == user2
False
정의 방법
- dataclasses 모듈에서 제공하는 @dataclass 데코레이터를 일반 클래스에 선언해주면 해당 클래스는 소위 데이터 클래스가 됩니다.
from dataclasses import dataclass
from datetime import date
@dataclass
class User:
id: int
name: str
birthdate: date
admin: bool = False
default value 할당 방법
dataclass
를 사용할 때 흔히 나오는 실수는 list와 같은 가변 데이터 타입의 필드에 기본값을 할당해줄 때 발생합니다.
- 필드의 기본값은 instance 간에 공유가 되기 떄문에, 아래와 같이 기본값 할당이 안된다.
@dataclass(unsafe_hash=True)
class User:
id: int
name: str
birthdate: date
admin: bool = False
friends: List[int] = []
ValueError: mutable default <class 'list'> for field friends is not allowed: use default_factory
- 이럴 때는
dataclasses
모듈에서 제공하는 filed
함수의 default_factory
옵션을 사용해서 매번 새로운 리스트가 생성될 수 있도록 해줘야 합니다.
from dataclasses import dataclass, field
from datetime import date
from typing import List
@dataclass(unsafe_hash=True)
class User:
id: int
name: str
birthdate: date
admin: bool = False
friends: List[int] = field(default_factory=list)
>>> user1 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user1.friends
[]
>>> user1.friends.append(2)
>>> user1.friends
[2]
dataclass 생성 시, 주의해야할 점
- 문제 정의: 아래의 경우가 문제임
- 인스턴스 생성시 인자로
_name
만 허용되고, name
은 허용 안됨
- 인스턴스 프린트 시
_name
으로 나오고, name
으로 안나옴
import dataclasses
@dataclasses.dataclass
class Test:
_name: str
@property
def name(self):
return self._name
@name.setter
def name(self, name):
assert name
self._name = name
test = Test(name='hi')
test = Test(_name=None)
print(test)
>> Test(_name=None)
- 해결 방안?
test = Test(name=None)
으로 하면 에러가 나고,
test.name
으로 해도 정상작동 / test._name
으로 해 정상작동
print(test)
하면 _name
은 출력 안해줌
- 1안
import dataclasses
from dataclasses import dataclass, field
@dataclass
class Test:
_name: str = field(init=False, repr=False, default='hi')
name: str = dataclasses.MISSING
def __post_init__(self):
if isinstance(self.name, property):
self.name = Test._name
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, name: str) -> None:
assert name
self._name = name
from dataclasses import dataclass
@dataclass
class A_w_default:
x: str = 'a'
class A(A_w_default):
@property
def x(self) -> str:
return self._x
@x.setter
def x(self, value: str):
assert value
self._x = value
- 참고 (번외): 하지만 또 아래의 경우는 잘 된다.
import dataclasses
@dataclasses.dataclass
class Test:
_name: str = 'a'
@property
def name(self):
return self._name
@name.setter
def name(self, name):
assert name
self._name = name
test = Test()
print(test.name)
dataclass의 parameter 정리
@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C:
__init__()
, __repr__()
, __eq__()
와 같은 메서드를 자동으로 생성 (default 값이 True임)
- 따라서 이 데이터 클래스는 다음과 같이 이전 섹션에서 손수 작성했던 클래스와 동일하게 작동하는 것을 알 수 있습니다.
user1 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
user1
>>>
User(id=1, name='Steve Jobs', birthdate=datetime.date(1955, 2, 24), admin=False)
user2 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
user1 == user2
>>>
True
frozen
옵션으로, mutable/immutable 객체 모두 생성 가능
- dataclass는 기본적으로 mutable이나,
frozen
옵션을 사용하면 immutable이 될 수 있다.
- 데이터를 변경해보려고 하면, 에러가 발생한다.
@dataclass(frozen=True)
class User:
id: int
name: str
birthdate: date
admin: bool = False
order
옵션을 통해, data간 대소비교를 가능해짐
from dataclasses import dataclass
from datetime import date
@dataclass(order=True)
class User:
id: int
name: str
birthdate: date
admin: bool = False
>>> user1 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user2 = User(id=2, name="Bill Gates", birthdate=date(1955, 10, 28))
>>> user1 < user2
True
>>> user1 > user2
False
>>> sorted([user2, user1])
[User(id=1, name='Steve Jobs', birthdate=datetime.date(1955, 2, 24), admin=False), User(id=2, name='Bill Gates', birthdate=datetime.date(1955, 10, 28), admin=False)]
unsafe_hash
옵션을 통해, data class 인스턴스를 hashable 하게 만들 수 있음
- 이를 통해, 데이터 간 중복 데이터 제거 가능
@dataclass(unsafe_hash=True)
class User:
id: int
name: str
birthdate: date
admin: bool = False
>>> user1 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user2 = User(id=2, name="Bill Gates", birthdate=date(1955, 10, 28))
>>> user3 = User(id=1, name="Steve Jobs", birthdate=date(1955, 2, 24))
>>> user4 = User(id=2, name="Bill Gates", birthdate=date(1955, 10, 28))
>>> set([user1, user2, user3, user4])
{User(id=2, name='Bill Gates', birthdate=datetime.date(1955, 10, 28), admin=False), User(id=1, name='Steve Jobs', birthdate=datetime.date(1955, 2, 24), admin=False)}
dataclass의 메서드
dataclasses.asdict(instance, *, dict_factory=dict)
@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}]}
dataclasses.astuple(instance, *, tuple_factory=tuple)
assert astuple(p) == (10, 20)
assert astuple(c) == ([(0, 0), (10, 4)],)