“더 적은 코드로 더 강력하게” 파이썬 데이터클래스 사용법
컨텐츠 정보
- 조회 433
본문
‘파이썬에서 모든 것은 객체’라는 말이 있다. 고유한 속성과 메서드가 있는 나만의 맞춤형 객체를 만들고 싶다면 파이썬의 class 객체를 사용한다. 그러나 간혹 파이썬에서 클래스를 만든다는 것은 예를 들어 전달된 매개변수로부터 클래스 인스턴스를 설정하거나, 비교 연산자 같은 일반적인 함수를 만드는 것과 같이 반복적이고 상투적인 코드를 대량으로 작성해야 한다는 것을 의미하기도 한다.
파이썬 3.7에 도입되고 파이썬 3.6으로도 백포트된 데이터클래스(dataclass)는 더 간편하고 덜 장황한 방법으로 클래스를 만들 수 있게 해준다. 예를 들어 클래스에 전달된 인자로부터 속성을 인스턴스화하는 것과 같이 클래스에서 일반적으로 하는 작업은 데이터클래스를 사용해 소수의 기본적인 지시문으로 축약할 수 있다.
파이썬 데이터클래스의 보이지 않는 강력함
다음과 같은 파이썬의 일반적인 클래스 예시를 보자.
class Book: '''Object for tracking physical books in a collection.''' def __init__(self, name: str, weight: float, shelf_id:int = 0): self.name = name self.weight = weight # in grams, for calculating shipping self.shelf_id = shelf_id def __repr__(self): return(f"Book(name={self.name!r}, weight={self.weight!r}, shelf_id={self.shelf_id!r})")여기서 가장 큰 골칫거리는 __init__에 전달된 각 인자를 객체의 속성으로 복사해야 한다는 점이다. Book 하나만 다룬다면 큰 문제는 아니지만 예를 들어 Bookshelf, Library, Warehouse 등 여러 부가적인 클래스가 있다면 이야기가 달라진다. 게다가 이런 코드를 손으로 직접 입력하다 보면 실수할 가능성도 높아진다.
동일한 클래스를 파이썬 데이터클래스로 구현하면 다음과 같다.
from dataclasses import dataclass@dataclassclass Book: '''Object for tracking physical books in a collection.''' name: str weight: float shelf_id: int = 0데이터클래스에서 속성, 이른바 ‘필드’를 지정하면 @dataclass 데코레이터가 이를 초기화하는 데 필요한 모든 코드를 자동으로 생성한다. 또한 각 속성의 타입 정보도 보존하므로 타입 정보를 검사하는 린팅 툴을 사용한다면 클래스 생성자에 올바른 종류의 변수가 전달되는지 확인할 수 있다.
@dataclass가 배후에서 하는 또 다른 일은 클래스의 일반적인 던더(dunder) 메서드를 위한 코드를 자동으로 생성하는 것이다. 위의 전통적인 클래스에서는 __repr__을 직접 만들어야 하지만 데이터클래스에서는 @dataclass 데코레이터가 __repr__을 알아서 생성한다. 원한다면 생성된 코드를 오버라이드할 수 있지만 대부분의 경우에는 직접 코드를 작성할 필요가 없다.
데이터클래스는 일단 생성되면 기능적으로 일반 클래스와 동일하다. 데이터클래스를 사용한다고 해서 성능 측면에서 불리한 점은 없다. 클래스를 데이터클래스로 선언할 때 미세한 성능상의 불이익이 발생하지만 이는 데이터클래스 객체가 생성될 때 한 번만 발생하는 페널티다.
고급 파이썬 데이터클래스 초기화
데이터클래스 데코레이터는 자체 초기화 옵션을 취할 수 있다. 대부분의 경우 지정할 필요 없지만 특정한 예외적 상황에서는 유용할 수 있다. 가장 쓰임새가 많은 옵션은 다음과 같다(모두 True/False).
frozen: 읽기 전용 클래스 인스턴스를 생성한다. 데이터가 일단 할당되면 수정할 수 없다. 데이터클래스 인스턴스를 해시 가능하게 만들어야 하는 경우 유용하며, 이를 통해 사전 키로 사용하는 등 다양한 활용이 가능하다.frozen을 설정하면 생성된 데이터클래스를 위한__hash__메서드도 자동으로 생성된다. (unsafe_hash=true를 사용하면 데이터클래스가 읽기 전용인지 여부와 관계없이 데이터클래스를 위한__hash__메서드를 생성할 수 있지만, 이 경우 안전하지 않은 동작이 발생한다.)slots: 클래스에 명시적으로 정의된 필드만 허용함으로써 데이터클래스 인스턴스의 메모리 사용량을 줄인다. 메모리 절감 효과는 규모가 커야 명확히 나타난다(예를 들어 특정 객체의 인스턴스를 수천 개 이상 생성할 때). 몇 개의 데이터클래스 인스턴스만 생성한다면 굳이 사용할 이유가 없다.kw_only: 클래스의 모든 필드를 키워드 전용으로 만든다. 따라서 위치 인자가 아닌 키워드 인자를 사용해 정의해야 한다. 사전을 통해 데이터클래스 인스턴스의 인자를 전달하는 방법으로 유용하다.
파이썬 데이터클래스 필드 맞춤 설정하기
데이터클래스의 기본 동작 방식은 대부분의 사용례에서 잘 통한다. 그러나 데이터클래스의 필드가 초기화되는 방식을 세밀하게 조정해야 할 때도 가끔 있다. 다음 코드 샘플은 이런 세부 조정을 위해 field 함수를 사용하는 방법을 보여준다.
from dataclasses import dataclass, fieldfrom typing import List@dataclassclass Book: '''Object for tracking physical books in a collection.''' name: str condition: str = field(compare=False) weight: float = field(default=0.0, repr=False) shelf_id: int = 0 chapters: List[str] = field(default_factory=list)field 인스턴스에 기본값을 설정하면 제공되는 매개변수에 따라 해당 필드가 설정되는 방식이 달라진다. field에 가장 일반적으로 사용되는 옵션은 다음과 같다(이 외의 다른 옵션도 있음).
default: 필드의 기본값을 설정한다. (1)field를 사용해 필드의 다른 매개변수를 변경하고 (2) 해당 필드에 기본값을 지정하고 싶을 때default를 사용한다. 위 예시에서는default를 사용해weight를 0.0으로 설정했다.default_factory: 매개변수를 받지 않고 필드의 기본값으로 사용되는 객체를 반환하는 함수의 이름을 제공한다. 예시에서는chapters를 빈 목록으로 만들었다.repr: 기본값은True이며, 필드가 데이터클래스에 대해 자동으로 생성되는__repr__에 표시될지를 제어한다. 예시에서는 책의weight가__repr__에 표시되지 않도록repr=False를 사용해서 생략했다.compare: 기본값은True이며, 데이터클래스에 대해 자동으로 생성되는 비교 메서드에 필드를 포함한다. 여기서는 두 책의 비교에서condition이 사용되지 않도록 하기 위해compare=False로 설정했다.
필드의 순서를 조정해서 기본값이 아닌 필드가 먼저 오도록 했다.
파이썬 데이터클래스 초기화 제어하기
이 시점에서 “데이터클래스의 __init__ 메서드가 자동으로 생성된다면 초기화 프로세스를 더 세밀하게 제어하려는 경우 어떻게 해야 할까?”라는 의문이 들 수 있다. 이런 경우에는 __post_init__ 메서드 또는 InitVar 타입을 사용할 수 있다.
__post_init__
데이터클래스 정의에 __post_init__ 메서드를 포함하면 필드나 다른 인스턴스 데이터를 수정하기 위한 지침을 제공할 수 있다.
from dataclasses import dataclass, fieldfrom typing import List@dataclassclass Book: '''Object for tracking physical books in a collection.''' name: str weight: float = field(default=0.0, repr=False) shelf_id: Optional[int] = field(init=False) chapters: List[str] = field(default_factory=list) condition: str = field(default="Good", compare=False) def __post_init__(self): if self.condition == "Discarded": self.shelf_id = None else: self.shelf_id = 0예시에서는 __post_init__ 메서드를 만들어 책의 condition이 "Discarded"로 초기화되는 경우 shelf_id를 None으로 설정했다. field를 사용해 shelf_id를 초기화하고, field에 init을 False로 전달했다. 즉, shelf_id는 __init__에서 초기화되지 않지만 전체 데이터클래스에는 information 타입으로 field에 등록된다.
InitVar
파이썬 데이터클래스 설정을 맞춤 설정하는 또 다른 방법은 InitVar 타입을 사용하는 것이다. InitVar를 사용하면 __init__, 그 다음 __post_init__에 전달되지만 클래스 인스턴스에는 저장되지 않는 필드를 지정할 수 있다.
InitVar를 사용하면 데이터클래스를 설정할 때 초기화 중에만 사용되는 매개변수를 취할 수 있다. 예를 들면 다음과 같다.
from dataclasses import dataclass, field, InitVarfrom typing import List@dataclassclass Book: '''Object for tracking physical books in a collection.''' name: str condition: InitVar[str] = "Good" weight: float = field(default=0.0, repr=False) shelf_id: int = field(init=False) chapters: List[str] = field(default_factory=list) def __post_init__(self, condition): if condition == "Unacceptable": self.shelf_id = None else: self.shelf_id = 0필드의 타입을 InitVar로 설정하면(서브타입이 실제 필드 타입) @dataclass는 해당 필드를 데이터클래스 필드로 만들지 않고__post_init__에 인자로 전달한다.
이 버전의 Book 클래스에서는 condition을 클래스 인스턴스의 필드로 저장하지 않는다. condition은 초기화 단계에서만 사용된다. condition이 "Unacceptable"로 설정됐다면 shelf_id를 None으로 설정하겠지만, condition 자체를 클래스 인스턴스에 저장하지는 않는다.
파이썬 데이터클래스를 사용할 때와 사용하지 말아야 할 때
데이터클래스를 사용하는 일반적인 시나리오 중 하나는 namedtuple을 대체하는 경우다. 데이터클래스는 namedtuple과 동일한 동작과 그 이상의 기능을 제공하며, @dataclass(frozen=True)를 데코레이터로 사용해서 namedtuple처럼 불변성으로 만들 수 있다.
가능한 또 다른 사용례는 다루기 거추장스러운 중첩된 사전을 중첩된 데이터클래스 인스턴스로 대체하는 것이다. Library라는 데이터클래스가 있고 그 안에 shelves 목록 속성이 있다면 ReadingRoom이라는 데이터클래스를 사용해 그 목록을 채운 다음 메서드를 추가해 중첩된 항목(예를 들어 특정 방의 선반에 있는 책)에 손쉽게 액세스할 수 있다.
물론 모든 파이썬 클래스가 데이터클래스일 필요는 없다. 클래스를 만드는 주된 목적이 데이터를 담기 위한 용기를 만드는 것이 아니라 일련의 정적 메서드들을 그룹으로 묶는 데 있다면 데이터클래스로 만들 필요가 없다. 예를 들어 파서에서 흔히 볼 수 있는 패턴은 추상 구문 트리를 받아 트리를 탐색하면서 노드 타입을 기준으로 클래스 내의 다양한 메서드를 호출하는 클래스를 두는 것이다. 파서 클래스 자체에는 거의 아무런 데이터도 없으므로 이런 경우에는 데이터클래스가 유용하지 않다.
dl-itworldkorea@foundryco.com
관련자료
-
링크
-
이전
-
다음






