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 definedfrozen 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 -10Dataclass 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
Boilerplate 제거: dataclass는
__init__,__repr__,__eq__등을 자동 생성하여 코드를 간결하게 만듭니다.Type Hints 필수: dataclass는 type hints를 기반으로 동작하므로 모든 필드에 타입을 명시해야 합니다.
Mutable 기본값 주의: list, dict 같은 가변 객체는
field(default_factory=...)를 사용해야 합니다.frozen=True 활용: 불변 객체가 필요하면
frozen=True를 사용하여 실수로 인한 수정을 방지하세요.post_init 검증: 데이터 검증이 필요하면
__post_init__메서드를 오버라이드하세요.
This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.