-
[Python] dataclasses 모듈 사용하기언어/파이썬 & 장고 2021. 2. 13. 20:15
들어가기 전에
파이썬 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) # 오류