deep-dive2025-02-15·9 min·203/348

Python Dataclass 활용법과 고급 기능

Python 3.7+의 dataclass 데코레이터를 활용하여 깔끔하고 효율적인 클래스를 작성하는 방법을 알아봅니다.

Python Dataclass 활용법과 고급 기능

Python의 dataclass는 __init__, __repr__, __eq__ 등의 메서드를 자동으로 생성하여 클래스 작성 시간을 줄여줍니다. 데이터를 저장하는 클래스를 작성할 때 특히 유용합니다.

Environment

$ python --version
Python 3.11.5

$ python -c "import sys; print(sys.version_info)"
sys.version_info(major=3, minor=11, micro=5, releaselevel='final', serial=0)

Problem: Boilerplate 코드가 많은的传统적인 클래스

dataclass 없이 작성된 클래스의 문제점을 살펴봅시다:

class UserProfile:
    def __init__(self, name, email, age, is_active=True):
        self.name = name
        self.email = email
        self.age = age
        self.is_active = is_active
    
    def __repr__(self):
        return (f"UserProfile(name='{self.name}', email='{self.email}', "
                f"age={self.age}, is_active={self.is_active})")
    
    def __eq__(self, other):
        if not isinstance(other, UserProfile):
            return False
        return (self.name == other.name and 
                self.email == other.email and 
                self.age == other.age)
    
    def __hash__(self):
        return hash((self.name, self.email, self.age))

이 코드는 동작하지만, 필드가 추가될 때마다 __init__, __repr__, __eq__를 모두 수정해야 합니다.

Analysis: dataclass가 생성하는 메서드

from dataclasses import dataclass, fields

@dataclass
class SampleClass:
    x: int
    y: str

# Check what methods were generated
print([m for m in dir(SampleClass) if m.startswith('__') and not m.startswith('___')])

출력:

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', 
 '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', 
 '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', 
 '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', 
 '__repr__', '__setattr__', '__sizeof__', '__str__', 'x', 'y']

Solution: Dataclass 기본 활용

기본 dataclass

from dataclasses import dataclass
from datetime import datetime

@dataclass
class User:
    id: int
    username: str
    email: str
    created_at: datetime = None
    
    def __post_init__(self):
        if self.created_at is None:
            self.created_at = datetime.now()

# Usage
user1 = User(1, "joel", "joel@example.com")
user2 = User(1, "joel", "joel@example.com")

print(user1)  
# User(id=1, username='joel', email='joel@example.com', created_at=2024-01-15 10:30:00.123456)

print(user1 == user2)  # True - based on all fields
print(hash(user1))     # Works because __eq__ is defined

frozen dataclass (immutable)

from dataclasses import dataclass

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

point = ImmutablePoint(1.0, 2.0)
print(point)  # ImmutablePoint(x=1.0, y=2.0)

# This will raise an error
try:
    point.x = 3.0
except AttributeError as e:
    print(f"Error: {e}")
    # Error: cannot assign to field 'x'

dataclass with inheritance

from dataclasses import dataclass, field
from typing import List

@dataclass
class BaseItem:
    id: int
    name: str

@dataclass
class Product(BaseItem):
    price: float
    tags: List[str] = field(default_factory=list)

@dataclass
class DigitalProduct(Product):
    download_url: str
    file_size_mb: int = 0

product = DigitalProduct(
    id=1, 
    name="Python Course", 
    price=49.99, 
    tags=["python", "programming"],
    download_url="https://example.com/download",
    file_size_mb=500
)

print(product)
# DigitalProduct(id=1, name='Python Course', price=49.99, 
#                tags=['python', 'programming'], 
#                download_url='https://example.com/download', 
#                file_size_mb=500)

Advanced: Field Options and Validation

field 옵션 커스터마이징

from dataclasses import dataclass, field
from typing import List, Dict

@dataclass
class Configuration:
    # Default value
    debug: bool = False
    
    # With default_factory for mutable defaults
    allowed_hosts: List[str] = field(default_factory=list)
    
    # Exclude from __eq__
    request_count: int = field(default=0, compare=False)
    
    # Exclude from __repr__
    internal_id: str = field(default="", repr=False)
    
    # Custom metadata
    db_url: str = field(
        default="sqlite:///db.sqlite3",
        metadata={"description": "Database connection URL"}
    )

config = Configuration(
    debug=True,
    allowed_hosts=["localhost", "example.com"]
)

print(config)
# Configuration(debug=True, allowed_hosts=['localhost', 'example.com'], 
#                db_url='sqlite:///db.sqlite3')

Post-init validation

from dataclasses import dataclass
from typing import Optional

@dataclass
class PositiveNumber:
    value: float
    
    def __post_init__(self):
        if self.value < 0:
            raise ValueError(f"Value must be positive, got {self.value}")

# Valid usage
num = PositiveNumber(42.5)
print(num)  # PositiveNumber(value=42.5)

# Invalid usage
try:
    negative = PositiveNumber(-10)
except ValueError as e:
    print(f"Error: {e}")
    # Error: Value must be positive, got -10

Dataclass to dict and JSON

from dataclasses import dataclass, asdict, field
import json

@dataclass
class APIResponse:
    status: int
    message: str
    data: dict = field(default_factory=dict)

response = APIResponse(
    status=200,
    message="Success",
    data={"users": [{"id": 1, "name": "Joel"}]}
)

# Convert to dict
response_dict = asdict(response)
print(response_dict)

# Convert to JSON
response_json = json.dumps(response_dict, indent=2)
print(response_json)

Lessons Learned

  1. Boilerplate 제거: dataclass는 __init__, __repr__, __eq__ 등을 자동 생성하여 코드를 간결하게 만듭니다.

  2. Type Hints 필수: dataclass는 type hints를 기반으로 동작하므로 모든 필드에 타입을 명시해야 합니다.

  3. Mutable 기본값 주의: list, dict 같은 가변 객체는 field(default_factory=...)를 사용해야 합니다.

  4. frozen=True 활용: 불변 객체가 필요하면 frozen=True를 사용하여 실수로 인한 수정을 방지하세요.

  5. post_init 검증: 데이터 검증이 필요하면 __post_init__ 메서드를 오버라이드하세요.


This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.