[Python] dataclasses 모듈 사용하기
들어가기 전에
파이썬 3.6부터 컴파일 언어와 같이 정적 타입을 미리 선언하여 사용할 수 있습니다.
from typing import Dict
def test(number: int, name: str) -> Dict[str, int]:
return {name: number}
test(1234, 'Kim')
# {'Kim': 1234}
test(1234, 1224) # 에러가 나진 않음
# {1224: 1234}
이는 개발을 할 때, hint를 줄 뿐이지 구문 오류와 같은 에러는 발생시키지 않습니다. 아래에서 설명할 때, 위 개념을 사용하여 설명합니다.
개요
파이썬 3.7부터 dataclasses 모듈이 도입되어 인스턴스 생성 시, 변수 할당부터 코드의 양을 줄일 수 있는 많은 기능이 생겼습니다.
AS-IS
클래스로 초기값을 받는다고 한다면 아래와 같이 작성을 합니다.
class User:
def __init__(self, number: int, name: str):
self.number = number
self.name = name
user1 = User(123, 'Kim')
print(user1)
# <__main__.User object at 0x7fa1381241c0>
user2 = User(123, 'Kim')
print(user2)
# <__main__.User object at 0x7fb5f8144910>
print(user1 == user2)
# False
인스턴스 변수 할당이 많아진다면 위의 self.number = number 와 같은 코드가 계속 생성을 해줘야 합니다. 또한 해당 객체 출력 시, 할당된 변수가 나오지 않게 됩니다. 또한 동일한 클래스에 동일한 값을 할당한 후 대소 비교를 하면 메모리 값으로 비교하기 때문에 같지 않다라고 나옵니다.
이를 의도하는대로 변경하면 아래와 같이 추가를 해야 합니다.
class User:
def __init__(self, number: int, name: str):
self.number = number
self.name = name
def __repr__(self):
return self.__class__.__qualname__ + f"(number={self.number!r}, name={self.name!r})"
def __eq__(self, other):
if other.__class__ is self.__class__:
return (self.number, self.name) == (other.number, other.name)
return NotImplemented
user1 = User(123, 'Kim')
print(user1)
# User(number=123, name='Kim')
user2 = User(123, 'Kim')
print(user2)
# User(number=123, name='Kim')
print(user1 == user2)
# True
초기값이 추가되면 될 수록 위와 같은 코드의 양이 늘어나게 되므로 상당히 불편하고 좋지 않습니다.
dataclasses 모듈
dataclasses 모듈을 위와 같은 불편사항을 전부 해소할 수 있습니다.
from dataclasses import dataclass
@dataclass
class User:
number: int
name: str
user1 = User(123, 'Kim')
print(user1)
# User(number=123, name='Kim')
user2 = User(123, 'Kim')
print(user2)
# User(number=123, name='Kim')
print(user1 == user2)
# True
dataclass를 선언한 다음, 사용할 클래스 위에 데코레이터로 추가만 해주면 끝입니다.
만약, 선언 후, 값을 변경할 수 없도록 불변 데이터를 지정하려면 dataclass 데코레이터에 frozen=True 옵션을 추가하면 됩니다.
from dataclasses import dataclass
@dataclass(frozen=True)
class User:
number: int
name: str
user1 = User(123, 'Kim')
print(user1)
user1.name = 'Lee' # dataclasses.FrozenInstanceError: cannot assign to field 'name'
만약, 선언된 클래스 간 대소비교나 정렬을 하려면 아래와 같이 order=True 옵션을 추가하면 됩니다.
from dataclasses import dataclass
@dataclass(order=True)
class User:
number: int
name: str
user1 = User(123, 'Bbb')
user2 = User(122, 'Aaa')
print(user1 > user2)
print(sorted([user1, user2]))
# True
# [User(number=122, name='Aaa'), User(number=123, name='Bbb')]
user1 = User(12, 'A')
user2 = User(12, 'B')
print(user1 < user2)
print(sorted([user1, user2]))
# True
# [User(number=12, name='A'), User(number=12, name='B')]
대소비교나 정렬은 선언된 변수의 순서대로 처리 되는 것을 알 수 있습니다.
dataclass는 hash를 지원하지 않는데 만약 set을 사용하고 싶다면 unsafe_hash=True 옵션을 추가하면 됩니다.
from dataclasses import dataclass
@dataclass(unsafe_hash=True)
class User:
number: int
name: str
user = User(123, 'Kim')
user1 = User(123, 'Kim')
user2 = User(122, 'Kim')
user3 = User(122, 'Lee')
print({user, user1, user2, user3}) # user와 user1 중복제거
# {User(number=122, name='Kim'), User(number=123, name='Kim'), User(number=122, name='Lee')}
해당 변수에 기본값을 할당하는 것은 아래와 같이 쉽게 진행됩니다.
from dataclasses import dataclass
@dataclass
class User:
number: int
name: str = 'Anonymous'
만약 list와 같은 컨테이너 타입의 빈 값을 기본값으로 할당할 땐, field 함수를 할당받아 사용해야 합니다.
from dataclasses import dataclass, field
from typing import List
@dataclass
class User:
number: int
name: str = 'Anonymous'
test: List[int] = field(default_factory=list)
user = User(number=122, name='Kim')
해당 데이터 클래스 타입을 tuple이나 dictionary 형태로 변경하고자 하면 asdict나 astuple을 선언 후 사용하면 간편하게 변경이 가능합니다.
from dataclasses import dataclass, field, asdict, astuple
from typing import List
@dataclass
class User:
number: int
name: str = 'Anonymous'
test: List[int] = field(default_factory=list)
user = User(number=122, name='Kim')
print(asdict(user))
# {'number': 122, 'name': 'Kim', 'test': []}
print(astuple(user))
# (122, 'Kim', [])
메소드 내에서 사용하기 위해 선언하는 선언은 아래와 같이 표현합니다.
from dataclasses import dataclass, field
@dataclass
class Work:
id: str = field(init=False)
work = Work()
# 인자값 추가 시, 오류
보통 init() 내에서 메소드를 호출하거나 값을 계산하고 넣는 등의 작업을 할 수 있는데 dataclass에서는 post_init을 호출하여 동일하게 진행할 수 있습니다.
from dataclasses import dataclass, field
@dataclass
class Work:
id: str = field(init=False, default='0')
def __post_init__(self):
self.id = '531'
a = Work()
print(a.id)
# 531
인스턴스 변수가 아닌 클래스 변수로 선언하고자 하면 아래와 같이 표현합니다.
from dataclasses import dataclass
from typing import ClassVar
@dataclass
class Work:
id: ClassVar[int] = 123
work = Work(11111) # 오류