견고한 Python - 5일차 User-Defined Types: Dataclasses

0

robust_python

목록 보기
5/5

User-Defined Types: Dataclasses

dataclasses는 heterogeneous(이질간의) collection 변수를 나타낸다. 즉, 다른 타입들의 변수들을 하나의 변수로 묶어 놓은 것이다. composite type으로 이는 서로 다른 타입을 가진 변수들을 어떠한 그룹으로 묶어내거나 이들 간의 관계를 나타내기 위해 만든다.

dataclass는 python3.7부터 도입되었으며 dataclasses 모듈에 있다. 사용방법은 @dataclass annotation으로 class를 annotate하면 된다. 필요한 것은 class의 변수들인데, 이 변수들은 dataclass가 맴버 변수로 만든다.

from dataclasses import dataclass

@dataclass
class MyFraction:
    numerator: int = 0
    denominator: int = 1

오해하지말자. numeratordenominator는 클래스 변수가 아니라 인스턴스의 맴버 변수가 되는 것이다. dataclass는 자동으로 이 맴버변수들의 생성자를 만들어줌으로 다음과 같이 호출이 가능하다. 만약, 생성자에서 이 값들을 넣어주지 않으면 기본 값이 들어간다.

fraction1 = MyFraction()
fraction2 = MyFraction(1,5)
print(fraction1) # MyFraction(numerator=0, denominator=1)
print(fraction2) # MyFraction(numerator=1, denominator=5)

dataclass를 사용하면 두 가지 special methods를 얻을 수 있는데, __str____repr__이다. 위에서 print로 출력했을 때 MyFraction(numerator=1, denominator=5)이런 결과가 나온 것은 __str____repr__ magic method 덕분이다.

또한 동등성 검사(equality)도 할 수 있다. 물론 동등성 검사를 위해서는 @dataclasseq옵션을 True로 설정해주어야 한다.

from dataclasses import dataclass

@dataclass(eq=True)
class MyFraction:
    numerator: int = 0
    denominator: int = 1
    
fraction1 = MyFraction(1,5)
fraction2 = MyFraction(1,5)
print(fraction1 == fraction2) # True

이는 magic method인 __eq__함수를 dataclass에서 설정해주었기 때문이다.

추가적으로 relational comparision도 가능하다. 즉, 비교가 가능하다는 것이다. 이를 위해서는 @dataclass 어노테이션에 orderTrue로 설정해주어야 한다.

from dataclasses import dataclass

@dataclass(eq=True, order=True)
class MyFraction:
    numerator: int = 0
    denominator: int = 1
    
fraction1 = MyFraction(2,3)
fraction2 = MyFraction(1,5)
print(fraction1 < fraction2) # False

크기 비교는 순서대로 이루어지므로 numerator를 먼저 비교하고 다음으로 denominator를 비교한다. 그런데, 사실 분수의 크기 비교는 이렇게 이루어지지 않는다. 따라서 새롭게 정의할 필요가 있는데, magic method인 __le__, __lt__, __gt__, __ge__함수를 구현하면 된다.

Immutability

dataclass를 변하지 않도록 만들 수 있는데, 이를 위해서는 @dataclassfrozenTrue로 설정해야 한다. 이렇게하면 dataclass의 어떤 값도 변경되지 않는다. 이는 frozenTrue로 설정하면 해당 dataclass가 hashable이 되기 때문에 hash값이 바뀌는 일이 생기면 이러한 연산에 대해 거부할 수 있게 된다. 즉, __hash__ magic method가 자동으로 설정되어 hashable된다는 것이다.

from dataclasses import dataclass

@dataclass(frozen=True)
class MyFraction:
    numerator: int = 0
    denominator: int = 1
    
fraction1 = MyFraction(2,3)
print(fraction1.numerator) # 
print(fraction1.denominator) # 
fraction1.denominator = 0 # dataclasses.FrozenInstanceError: cannot assign to field 'denominator'

다만, frozen은 맴버 변수의 값이 변하는 것을 막을 뿐이지 맴버 변수의 맴버 변수는 바꿀 수 있다. 또한, list나 dict의 경우는 데이터를 추가할 수 있다. 즉, frozendataclass의 맴버 변수 자체가 다른 것으로 변경되는 것을 막는 것이지, 맴버 변수의 내부 동작까지 막을 수 있는 것은 아니다.

from dataclasses import dataclass

@dataclass
class Desc:
    description: str

@dataclass(frozen=True)
class MyFraction:
    numerator: int
    denominator: int
    desc: Desc
    author: list[str]
    
fraction1 = MyFraction(2,3, Desc("hello"), ["gyu", "gyu2"])
fraction1.desc.description = "bye"
fraction1.author.append("gyu3")
print(fraction1.numerator) # 2
print(fraction1.denominator) # 3 
print(fraction1.desc.description) # bye
print(fraction1.author) # ['gyu', 'gyu2', 'gyu3']
fraction1.desc = Desc("no") # dataclasses.FrozenInstanceError: cannot assign to field 'desc'

다음의 예제를 보면 fraction1.desc.description을 변경하는 것과 fraction1.author에 새로운 값을 추가하는 것은 문제가 없이 동작한다. 그러나 fraction1.desc = Desc("no")을 보면 맴버 변수에 새로운 값(인스턴스)를 할당하는 것은 안된다.

0개의 댓글