파이썬 dataclass 심화 활용 가이드

파이썬 3.7부터 표준 라이브러리에 추가된 dataclasses 모듈은 데이터를 저장하는 클래스(데이터 컨테이너)를 작성할 때 발생하는 보일러플레이트 코드(boilerplate code)를 크게 줄여줍니다. 이는 기본적으로 값을 가진 가변적인 namedtuple와 유사하지만, 타입 힌트(type hinting) 지원, 기본값 설정, 가변성(mutability) 제어 등 더 강력한 기능을 제공합니다.

기본 클래스와의 비교

일반적인 클래스를 정의하여 데이터를 저장할 때는 __init__ 메서드에서 속성을 직접 할당해야 합니다. 하지만 @dataclass 데코레이터를 사용하면 타입 힌트만으로도 이러한 초기화 코드를 자동으로 생성할 수 있습니다.

from dataclasses import dataclass

# 기존 방식
class UserOld:
    def __init__(self, username: str, email: str):
        self.username = username
        self.email = email

# dataclass 사용
@dataclass
class User:
    username: str
    email: str

위 두 클래스는 기능적으로 거의 동일하지만, User 클래스는 훨씬 간결하며 자동으로 __repr____eq__ 메서드도 생성됩니다.

초기화 후 처리: __post_init__

데이터클래스는 __init__ 메서드를 자동 생성하지만, 초기화 과정에서 추가적인 로직이 필요할 경우 __post_init__ 메서드를 정의할 수 있습니다. 이 메서드는 __init__가 실행된 직후에 호출됩니다.

@dataclass
class Item:
    name: str
    price: float
    vat: float = 0.1
    
    final_price: float = field(init=False)

    def __post_init__(self):
        self.final_price = self.price * (1 + self.vat)

product = Item("Laptop", 1000)
print(product.final_price)  # 1100.0 출력

위 예제에서 final_price는 초기화 인자로 받지 않지만(init=False), __post_init__을 통해 계산되어 할당됩니다.

데이터클래스 데코레이터 옵션

@dataclass 데코레이터는 클래스의 동작 방식을 제어하는 여러 파라미터를 받습니다.

  • init: __init__ 메서드 생성 여부 (기본값: True)
  • repr: __repr__ 메서드 생성 여부 (기본값: True)
  • eq: __eq__ 메서드 생성 여부 (기본값: True, 필드 값 비교)
  • order: 비교 연산자(<, <=, >, >=) 생성 여부 (기본값: False)
  • frozen: True일 경우 인스턴스가 불변(Immutable) 객체가 되어 생성 후 속성 변경 불가 (기본값: False)
  • unsafe_hash: 해시 함수 생성 강제화 (기본값: False)

frozen=True로 설정하면 데이터 무결성을 보장해야 하는 상황에서 유용하게 사용할 수 있습니다.

@dataclass(frozen=True, order=True)
class Score:
    value: int

s1 = Score(90)
s2 = Score(85)
# s1.value = 95  # FrozenInstanceError 발생
print(s1 > s2)  # True (order=True 덕분에 비교 가능)

필드 세부 제어: field()

대부분의 경우 타입 힌트와 기본값만으로 충분하지만, 때로는 속성의 동작을 더 세밀하게 제어해야 할 때가 있습니다. 이때 dataclasses.field() 함수를 사용합니다.

특히 리스트나 딕셔너리 같은 가변 객체를 기본값으로 사용할 때는 default_factory를 반드시 사용해야 합니다. 그렇지 않으면 모든 인스턴스가 같은 객체를 참조하는 오류가 발생합니다.

from dataclasses import dataclass, field
from typing import List

@dataclass
class Team:
    name: str
    members: List[str] = field(default_factory=list)

team_a = Team("Alpha")
team_b = Team("Beta")

team_a.members.append("Alice")
print(team_b.members)  # [] (team_a에만 추가됨)

field()의 주요 매개변수는 다음과 같습니다.

  • default: 필드의 기본값 설정
  • default_factory: 기본값을 생성하는 호출 가능한 객체(callable) 지정
  • init: __init__ 인자에 포함할지 여부
  • repr: 문자열 표현에 포함할지 여부
  • compare: 비교 연산(__eq__ 등)에 포함할지 여부

초기화 전용 변수: InitVar

데이터를 저장할 필요는 없지만 초기화 과정에서 필요한 값이 있을 수 있습니다. InitVar로 타입을 명시하면 해당 필드는 초기화 로직에만 사용되고, 최종 인스턴스에는 저장되지 않습니다.

from dataclasses import InitVar

@dataclass
class Connection:
    db_host: str
    password: InitVar[str]
    is_connected: bool = field(init=False, default=False)

    def __post_init__(self, password: str):
        if password == "admin123":
            self.is_connected = True

conn = Connection("localhost", "admin123")
# conn.password 속성은 존재하지 않음
print(conn.is_connected)  # True

유틸리티 함수

dataclasses 모듈은 데이터클래스 인스턴스를 다루기 위한 유용한 함수들을 제공합니다.

  • asdict(instance): 데이터클래스 인스턴스를 딕셔너리로 변환합니다.
  • astuple(instance): 데이터클래스 인스턴스를 튜플로 변환합니다.
  • is_dataclass(instance): 해당 객체가 데이터클래스인지 확인합니다.
from dataclasses import asdict, astuple, is_dataclass

@dataclass
class Point:
    x: int
    y: int

p = Point(10, 20)
print(asdict(p))   # {'x': 10, 'y': 20}
print(astuple(p))  # (10, 20)
print(is_dataclass(p))  # True

상속(Inheritance)

데이터클래스도 일반 클래스처럼 상속이 가능합니다. 자식 클래스가 데이터클래스 데코레이터를 사용하는 경우, 부모 클래스의 필드를 상속받고, 동일한 이름의 필드가 존재하면 자식 클래스의 필드가 부모의 것을 덮어씁니다. 생성된 메서드들도 이 상속 관계를 반영하여 통합적으로 생성됩니다.

@dataclass
class Entity:
    id: int
    name: str

@dataclass
class Player(Entity):
    score: int = 0
    name: str = "Unknown"  # 부모의 name 오버라이딩

p = Player(1, "Hero", 100)
print(p)  # Player(id=1, name='Hero', score=100)

데이터클래스를 적절히 활용하면 반복적인 코드 작성을 줄이고 가독성을 높일 수 있습니다. 다만, 가변 객체를 기본값으로 사용할 때는 default_factory를 사용하는 것과, 불변 객체가 필요할 때 frozen=True를 설정하는 등의 주의사항을 숙지해야 합니다.

태그: python dataclass Python3.7 OOP programming

8월 28일 08:29에 게시됨