Python 데이터클래스

왜 dataclass?

# 기존 방식: 보일러플레이트 코드가 많음
class User:
    def __init__(self, name, age, email=""):
        self.name = name
        self.age = age
        self.email = email

    def __repr__(self):
        return f"User(name={self.name!r}, age={self.age!r}, email={self.email!r})"

    def __eq__(self, other):
        if not isinstance(other, User):
            return False
        return self.name == other.name and self.age == other.age and self.email == other.email

# dataclass: 위 코드와 동일한 기능, 단 4줄
@dataclass
class User:
    name: str
    age: int
    email: str = ""
장점설명
간결함__init__, __repr__, __eq__ 자동 생성
타입 힌트필드 타입 명시로 가독성/IDE 지원 향상
불변성frozen=True로 쉽게 불변 객체 생성
비교/정렬order=True로 비교 연산자 자동 생성
유틸리티asdict(), replace() 등 편의 함수 제공

기본 사용법

from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int
    email: str = ""  # 기본값

# __init__, __repr__, __eq__ 자동 생성
user = User("Alice", 25)
user    # User(name='Alice', age=25, email='')

user1 = User("Alice", 25)
user2 = User("Alice", 25)
user1 == user2  # True (__eq__ 자동 생성)

field 옵션

from dataclasses import dataclass, field

@dataclass
class Post:
    title: str
    content: str
    tags: list = field(default_factory=list)  # 가변 객체 기본값
    views: int = field(default=0, repr=False)  # repr에서 제외
    _id: int = field(default=0, compare=False)  # 비교에서 제외

# field 주요 옵션
field(
    default=...,           # 기본값
    default_factory=...,   # 가변 객체 기본값 (list, dict 등)
    repr=True,             # __repr__에 포함 여부
    compare=True,          # __eq__에 포함 여부
    hash=None,             # __hash__에 포함 여부
    init=True,             # __init__에 포함 여부
)

불변 객체 (frozen)

@dataclass(frozen=True)
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
p.x = 3.0  # FrozenInstanceError! (수정 불가)

# frozen=True → __hash__ 자동 생성 (딕셔너리 키로 사용 가능)
points = {Point(0, 0): "origin"}

post_init

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)  # __init__에서 제외

    def __post_init__(self):
        self.area = self.width * self.height

rect = Rectangle(3, 4)
rect.area  # 12

상속

@dataclass
class Person:
    name: str
    age: int

@dataclass
class Employee(Person):
    employee_id: str
    department: str = "General"

emp = Employee("Alice", 30, "E001")
emp  # Employee(name='Alice', age=30, employee_id='E001', department='General')

데코레이터 옵션

@dataclass(
    init=True,        # __init__ 생성
    repr=True,        # __repr__ 생성
    eq=True,          # __eq__ 생성
    order=False,      # __lt__, __le__, __gt__, __ge__ 생성
    frozen=False,     # 불변 객체
    slots=False,      # __slots__ 사용 (Python 3.10+)
    kw_only=False,    # 모든 필드를 키워드 전용으로 (Python 3.10+)
)
class MyClass:
    ...

# order=True 예시
@dataclass(order=True)
class Version:
    major: int
    minor: int
    patch: int

Version(2, 0, 0) > Version(1, 9, 9)  # True

유틸리티 함수

from dataclasses import asdict, astuple, fields, replace

@dataclass
class User:
    name: str
    age: int

user = User("Alice", 25)

# 딕셔너리/튜플 변환
asdict(user)   # {'name': 'Alice', 'age': 25}
astuple(user)  # ('Alice', 25)

# 필드 정보 조회
fields(user)   # (Field(name='name',...), Field(name='age',...))

# 복사 후 일부 값 변경
new_user = replace(user, age=26)
new_user  # User(name='Alice', age=26)

slots (Python 3.10+)

@dataclass(slots=True)
class Point:
    x: float
    y: float

# 장점: 메모리 절약, 속성 접근 속도 향상
# 단점: 동적 속성 추가 불가
p = Point(1.0, 2.0)
p.z = 3.0  # AttributeError!

실전 패턴

# 설정 객체
@dataclass(frozen=True)
class Config:
    host: str = "localhost"
    port: int = 8080
    debug: bool = False

# DTO (Data Transfer Object)
@dataclass
class UserDTO:
    id: int
    username: str
    email: str

    @classmethod
    def from_dict(cls, data: dict):
        return cls(**data)

    def to_dict(self):
        return asdict(self)

# JSON 직렬화
import json

@dataclass
class Item:
    name: str
    price: float

item = Item("Book", 29.99)
json.dumps(asdict(item))  # '{"name": "Book", "price": 29.99}'