dataclass 정복!

About_work·2023년 2월 15일
0

python 기초

목록 보기
17/55

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>
    • 인스턴스 간 equality 비교가 안된다.
>>> 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
# # 문법 오류:  객체 생성과 동시에 name 설정 불가
test = Test(name='hi')
# 잘못된 초기값: 객체 생성 시, name 설정은 가능하나 name이 _name으로 설정되는 것을 알아야 하고, 
# assert 조건을 bypass 함
test = Test(_name=None)   
# 로깅 상 출력시, name이 아닌 _name 필드로 출력됨.
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
  • 2안
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) # a

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)],)
profile
새로운 것이 들어오면 이미 있는 것과 충돌을 시도하라.

0개의 댓글