ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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) # 오류

    댓글