데이터 클래스

파이썬 3.7 의 안정판 출시를 앞두고 있는 시점이라 한글 도큐멘테이션을 그에 맞추어 업데이트하느라 꽤 시간과 노력을 들이고 있습니다. 어떤 변화가 있는지는 파이썬 3.7 의 새로운 기능은? 에 정리되어 있습니다.

모든 변경이 중요하겠지만, 그중 눈에 띄는 것은 새로 추가된 dataclasses 모듈입니다. 쓰기도 쉽고 그 용도도 광범위합니다. 3.6 버전을 위한 백포트도 제공되는데 이렇게 설치하면 됩니다:

pip install dataclasses

아쉽지만, 변수 어노테이션을 필요로 하는 모듈이기 때문에, 파이썬 3.5 나 그 이전 버전에서는 지원할 방법이 없습니다.

자 이제 어떤 녀석인지 조금 더 들여다봅시다.

dataclasses 모듈은 dataclasses.dataclass() 라는 이름의 데코레이터를 제공합니다. 이런식으로 씁니다.

>>> from dataclasses import dataclass
>>>
>>> @dataclass
... class Book:
...     title: str
...     price: int = None
...
>>> book = Book('당신은 데이터의 주인이 아니다')
>>> book
Book(title='당신은 데이터의 주인이 아니다', price=None)

데이터 클래스는 변수 어노테이션이 붙어있는 어트리뷰트들을 찾습니다. 여기서는 titleprice 옆에 붙어있는 : str: int 부분을 뜻합니다. (3.6 에서 새로 소개된 문법입니다.) 이 것들을 필드라고 부릅니다. 이제 이 필드들을 사용하는 __init____repr__ 을 정의해줍니다. 가령 __init__title 을 필수 (기본값이 없어서), price 를 선택 (기본값이 있어서) 매개변수로 정의합니다. 그래서 Book('당신은 데이터의 주인이 아니다') 라는 표현을 쓸 수 있게됩니다. Book(title='당신은 데이터의 주인이 아니다', price=None)dataclasses.dataclass() 가 만들어준 __repr__ 의 출력입니다.

데이터 클래스를 처음 접할때, 흔히 변수 어노테이션으로 지정해준 형을 감사하리라고 오해합니다. 데이터 클래스에게 (특수한 경우를 제외하고는) 이 형 자체는 의미가 없고, 일체의 검사나 변환을 수행하지 않습니다.

>>> book.price = '19000원'
>>> book
Book(title='당신은 데이터의 주인이 아니다', price='19000원')

이 외에 __eq__ 도 만들어줍니다.

>>> book == Book(title='당신은 데이터의 주인이 아니다')
True
>>> book.price = 19000
>>> book
Book(title='당신은 데이터의 주인이 아니다', price=19000)
>>> book == Book(title='당신은 데이터의 주인이 아니다')
False

__eq__ 가 정의되지 않는 경우 클래스 인스턴스는 id() 로 비교합니다. 가령:

>>> class X: pass
...
>>> X() == X()
False
>>> @dataclass
... class Y: pass
...
>>> Y() == Y()
True

dataclasses.dataclass()order=True 를 제공하면 비교연산자도 정의해줍니다.

>>> @dataclass(order=True)
... class Book:
...     title: str
...     price: int = None
...
>>> book1 = Book('당신은 데이터의 주인이 아니다', 19000)
>>> book2 = Book('당신은 데이터의 주인이 아니다', 17100)
>>> book1 > book2
True

대소비교에는 필드들의 순서가 중요합니다. 필드들의 튜플을 대소비교하는 것과 같은 결과를 얻게 됩니다.

이렇게 정의한 데이터 클래스는 여전히 해시가능하지 않아서 딕셔너리의 키나, 집합의 원소로 사용할 수가 없습니다.

>>> hash(book1)
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'Book'
>>> {book1}
Traceback (most recent call last):
  ...
TypeError: unhashable type: 'Book'

가변 객체니 당연한 결과입니다. frozen=True 옵션을 추가하면 불변 객체로 만들어줍니다.

>>> @dataclass(order=True, frozen=True)
... class Book:
...     title: str
...     price: int = None
...
>>> book == Book(title='당신은 데이터의 주인이 아니다')
>>> hash(book)
-4964120138028002284
>>> {book}
{Book(title='당신은 데이터의 주인이 아니다', price=None)}
>>> book.price = 19000
Traceback (most recent call last):
  ...
dataclasses.FrozenInstanceError: cannot assign to field 'price'

맺음말

데이터 클래스는 변수 어노테이션 을 활용해서 필드들을 문서화하고, 반복적으로 만들게되는 특수 메서드들을 자동 정의해주는 편의를 제공합니다. 어찌보면 문법적으로는 아주 다르지만 collections.namedtuple() 의 가변형이라고 볼 수도 있습니다. collections.namedtuple() 은 새 클래스를 만들지만, 데이터 클래스는 특수 메서드를 추가할뿐 클래스 자체는 그대로 사용합니다. 때문에 여러분의 입맛에 맞게 이런 저런 메서드나 프로퍼티들을 추가할 수 있습니다. 필자는 지금 두가지 목적에 활용하고 있습니다. 로그 파일을 파싱하는 라이브러리에서 사용하고 있고, Pandas 데이터 프레임을 관리하는 용도로도 쓰고 있습니다. 이런 용도로 사용할 경우, 데이터 클래스가 제공하지 않고 있는 형변환 기능을 추가하면 편리합니다.

이 외에도 다양한 요구사항에 대응하기 위해 여러 가지 기능들이 함께 제공되고 있습니다. 자세한 내용은 dataclasses 를 참조하세요.