13. 데이터클래스 (Dataclasses)
13. 데이터클래스 (Dataclasses)¶
학습 목표¶
- dataclasses 모듈의 목적과 장점 이해
- @dataclass 데코레이터와 옵션 마스터
- field() 함수를 활용한 고급 필드 설정
- 상속, 불변성, 슬롯 등 고급 기능 활용
- Pydantic과의 비교 및 선택 기준 이해
목차¶
1. 데이터클래스 기초¶
1.1 데이터클래스란?¶
# 일반 클래스로 데이터 구조 정의
class PersonManual:
def __init__(self, name: str, age: int, email: str):
self.name = name
self.age = age
self.email = email
def __repr__(self):
return f"PersonManual(name={self.name!r}, age={self.age}, email={self.email!r})"
def __eq__(self, other):
if not isinstance(other, PersonManual):
return NotImplemented
return (self.name, self.age, self.email) == (other.name, other.age, other.email)
# 데이터클래스로 동일한 기능 (간결!)
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
email: str
1.2 자동 생성 메서드¶
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float
# 자동 생성되는 메서드들:
# 1. __init__
p = Point(3.0, 4.0)
# 2. __repr__
print(p) # Point(x=3.0, y=4.0)
# 3. __eq__
p1 = Point(3.0, 4.0)
p2 = Point(3.0, 4.0)
print(p1 == p2) # True
# 비교에 사용되는 것은 모든 필드
p3 = Point(3.0, 5.0)
print(p1 == p3) # False
1.3 기본값 설정¶
from dataclasses import dataclass
from typing import Optional
@dataclass
class User:
name: str
email: str
age: int = 0 # 기본값
active: bool = True # 기본값
nickname: Optional[str] = None # Optional with default
# 기본값 있는 필드는 기본값 없는 필드 뒤에 와야 함
user = User("Alice", "alice@example.com")
print(user) # User(name='Alice', email='alice@example.com', age=0, active=True, nickname=None)
user2 = User("Bob", "bob@example.com", age=30, nickname="bobby")
2. @dataclass 옵션¶
2.1 주요 옵션¶
@dataclass(
init=True, # __init__ 생성 (기본: True)
repr=True, # __repr__ 생성 (기본: True)
eq=True, # __eq__ 생성 (기본: True)
order=False, # 비교 메서드 생성 (기본: False)
unsafe_hash=False, # __hash__ 생성 (기본: False)
frozen=False, # 불변 객체 (기본: False)
match_args=True, # 패턴 매칭 지원 (Python 3.10+)
kw_only=False, # 모든 필드 키워드 전용 (Python 3.10+)
slots=False, # __slots__ 사용 (Python 3.10+)
)
class MyClass:
pass
2.2 order - 비교 연산자¶
@dataclass(order=True)
class Student:
name: str
grade: float
age: int
students = [
Student("Alice", 3.8, 20),
Student("Bob", 3.5, 22),
Student("Charlie", 3.8, 21),
]
# 정렬 (모든 필드 순서대로 비교)
sorted_students = sorted(students)
# name으로 먼저 비교, 같으면 grade, 같으면 age
# 특정 필드로 정렬
sorted_by_grade = sorted(students, key=lambda s: s.grade, reverse=True)
2.3 frozen - 불변 객체¶
@dataclass(frozen=True)
class ImmutablePoint:
x: float
y: float
point = ImmutablePoint(1.0, 2.0)
# 수정 시도시 에러
try:
point.x = 3.0
except AttributeError as e:
print(f"Error: {e}") # cannot assign to field 'x'
# 불변이므로 해시 가능 (dict 키, set 요소로 사용 가능)
points = {point: "origin"}
point_set = {point, ImmutablePoint(3.0, 4.0)}
2.4 slots - 메모리 최적화¶
@dataclass(slots=True) # Python 3.10+
class OptimizedPoint:
x: float
y: float
# slots=True 장점:
# 1. 메모리 사용량 감소
# 2. 속성 접근 속도 향상
# 3. __dict__ 없음 (동적 속성 추가 불가)
point = OptimizedPoint(1.0, 2.0)
# point.z = 3.0 # AttributeError - slots에 없는 속성
# 메모리 비교
import sys
@dataclass
class RegularPoint:
x: float
y: float
regular = RegularPoint(1.0, 2.0)
optimized = OptimizedPoint(1.0, 2.0)
print(sys.getsizeof(regular.__dict__)) # dict 크기
# OptimizedPoint는 __dict__ 없음
2.5 kw_only - 키워드 전용 인자¶
@dataclass(kw_only=True) # Python 3.10+
class Config:
host: str
port: int
debug: bool = False
# 반드시 키워드로 전달
config = Config(host="localhost", port=8080)
# config = Config("localhost", 8080) # TypeError
3. field() 함수¶
3.1 field() 기본 사용¶
from dataclasses import dataclass, field
from typing import List
@dataclass
class Team:
name: str
members: List[str] = field(default_factory=list) # 가변 기본값
score: int = field(default=0)
# 잘못된 방법 (가변 객체 직접 할당)
# members: List[str] = [] # 모든 인스턴스가 같은 리스트 공유!
team1 = Team("Alpha")
team2 = Team("Beta")
team1.members.append("Alice")
print(team2.members) # [] - 올바르게 분리됨
3.2 field() 옵션¶
from dataclasses import dataclass, field
@dataclass
class Product:
# 기본 필드
name: str
# 기본값 팩토리
tags: list = field(default_factory=list)
# repr에서 제외
internal_id: str = field(repr=False, default="")
# 비교에서 제외
cache: dict = field(compare=False, default_factory=dict)
# __init__에서 제외 (후처리로 설정)
computed: str = field(init=False)
# 해시에서 제외 (frozen=True 시)
mutable_data: list = field(hash=False, default_factory=list)
# 메타데이터 추가
price: float = field(metadata={"unit": "USD", "min": 0})
def __post_init__(self):
self.computed = f"{self.name}_computed"
product = Product("Widget", price=9.99)
print(product) # internal_id 숨김
print(product.computed) # Widget_computed
# 메타데이터 접근
from dataclasses import fields
for f in fields(Product):
if f.metadata:
print(f"{f.name}: {f.metadata}")
3.3 post_init 활용¶
from dataclasses import dataclass, field
@dataclass
class Rectangle:
width: float
height: float
area: float = field(init=False)
perimeter: float = field(init=False)
def __post_init__(self):
self.area = self.width * self.height
self.perimeter = 2 * (self.width + self.height)
rect = Rectangle(5, 3)
print(f"Area: {rect.area}") # 15
print(f"Perimeter: {rect.perimeter}") # 16
3.4 InitVar - 초기화 전용 변수¶
from dataclasses import dataclass, field, InitVar
@dataclass
class User:
name: str
email: str
password: InitVar[str] # 초기화에만 사용, 필드로 저장 안 됨
password_hash: str = field(init=False)
def __post_init__(self, password: str):
import hashlib
self.password_hash = hashlib.sha256(password.encode()).hexdigest()
user = User("Alice", "alice@example.com", "secret123")
print(user) # password 없음, password_hash 있음
# User(name='Alice', email='alice@example.com', password_hash='...')
# user.password # AttributeError - InitVar는 저장되지 않음
4. 고급 기능¶
4.1 데이터클래스 유틸리티 함수¶
from dataclasses import dataclass, field, fields, asdict, astuple, replace
@dataclass
class Person:
name: str
age: int
email: str = ""
person = Person("Alice", 30, "alice@example.com")
# fields(): 필드 정보 조회
for f in fields(person):
print(f"Name: {f.name}, Type: {f.type}, Default: {f.default}")
# asdict(): 딕셔너리로 변환
person_dict = asdict(person)
print(person_dict) # {'name': 'Alice', 'age': 30, 'email': 'alice@example.com'}
# astuple(): 튜플로 변환
person_tuple = astuple(person)
print(person_tuple) # ('Alice', 30, 'alice@example.com')
# replace(): 일부 필드만 변경한 새 인스턴스 생성
person2 = replace(person, name="Bob", age=25)
print(person2) # Person(name='Bob', age=25, email='alice@example.com')
4.2 커스텀 hash¶
@dataclass(eq=True, frozen=True)
class HashableItem:
id: int
name: str
# frozen=True면 자동으로 __hash__ 생성
# 또는 수동 해시
@dataclass(eq=True)
class CustomHashItem:
id: int
name: str
data: list # 가변 필드
def __hash__(self):
return hash(self.id) # id만으로 해시
item = CustomHashItem(1, "test", [1, 2, 3])
items_set = {item} # 해시 가능
4.3 JSON 직렬화¶
from dataclasses import dataclass, asdict
import json
from datetime import datetime
from typing import List
@dataclass
class Event:
name: str
date: datetime
attendees: List[str]
def serialize_event(event: Event) -> str:
def encoder(obj):
if isinstance(obj, datetime):
return obj.isoformat()
raise TypeError(f"Object of type {type(obj)} is not JSON serializable")
return json.dumps(asdict(event), default=encoder)
def deserialize_event(json_str: str) -> Event:
data = json.loads(json_str)
data["date"] = datetime.fromisoformat(data["date"])
return Event(**data)
event = Event("Meeting", datetime.now(), ["Alice", "Bob"])
json_str = serialize_event(event)
print(json_str)
restored = deserialize_event(json_str)
print(restored)
5. 상속과 조합¶
5.1 데이터클래스 상속¶
from dataclasses import dataclass
@dataclass
class Animal:
name: str
age: int
@dataclass
class Dog(Animal):
breed: str
is_trained: bool = False
dog = Dog("Buddy", 3, "Labrador")
print(dog) # Dog(name='Buddy', age=3, breed='Labrador', is_trained=False)
5.2 기본값과 상속 주의점¶
from dataclasses import dataclass, field
@dataclass
class Base:
x: int
y: int = 0 # 기본값 있음
# 오류! 기본값 없는 필드가 기본값 있는 필드 뒤에 올 수 없음
# @dataclass
# class Derived(Base):
# z: int # Error!
# 해결책 1: 기본값 제공
@dataclass
class Derived1(Base):
z: int = 0
# 해결책 2: kw_only 사용 (Python 3.10+)
@dataclass
class Derived2(Base):
z: int = field(kw_only=True, default=0)
5.3 조합 (Composition)¶
from dataclasses import dataclass
from typing import List
@dataclass
class Address:
street: str
city: str
country: str
postal_code: str
@dataclass
class ContactInfo:
email: str
phone: str
@dataclass
class Employee:
name: str
employee_id: str
address: Address # 조합
contact: ContactInfo # 조합
skills: List[str] = field(default_factory=list)
employee = Employee(
name="Alice",
employee_id="E001",
address=Address("123 Main St", "Seoul", "Korea", "12345"),
contact=ContactInfo("alice@company.com", "010-1234-5678"),
skills=["Python", "SQL"]
)
5.4 믹스인 패턴¶
from dataclasses import dataclass, field
from datetime import datetime
class TimestampMixin:
"""타임스탬프 기능 믹스인"""
created_at: datetime = field(default_factory=datetime.now)
updated_at: datetime = field(default_factory=datetime.now)
def touch(self):
self.updated_at = datetime.now()
@dataclass
class Article(TimestampMixin):
title: str
content: str
author: str
article = Article("Hello", "World", "Alice")
print(article.created_at)
article.touch()
print(article.updated_at)
6. Pydantic 비교¶
6.1 dataclass vs Pydantic¶
# dataclass - 간단한 데이터 구조
from dataclasses import dataclass
@dataclass
class UserDataclass:
name: str
age: int
email: str
# 타입 검증 없음!
user = UserDataclass("Alice", "not an int", "invalid-email")
print(user) # 에러 없이 생성됨
# Pydantic - 런타임 검증
from pydantic import BaseModel, EmailStr, Field
class UserPydantic(BaseModel):
name: str
age: int = Field(ge=0, le=150)
email: EmailStr
try:
user = UserPydantic(name="Alice", age="25", email="alice@example.com")
print(user) # age가 자동으로 int로 변환
print(user.age, type(user.age)) # 25 <class 'int'>
except Exception as e:
print(f"Validation error: {e}")
6.2 선택 기준¶
┌─────────────────────────────────────────────────────────────────┐
│ dataclass vs Pydantic 선택 │
│ │
│ dataclass 선택: │
│ - 내부 데이터 구조 │
│ - 성능이 중요한 경우 │
│ - 단순한 데이터 컨테이너 │
│ - 외부 의존성 최소화 │
│ - 타입 검증이 필요 없는 경우 │
│ │
│ Pydantic 선택: │
│ - API 입력 검증 │
│ - 설정 파일 파싱 │
│ - 외부 데이터 처리 │
│ - JSON 직렬화/역직렬화 │
│ - 복잡한 검증 로직 │
└─────────────────────────────────────────────────────────────────┘
6.3 Pydantic dataclass¶
from pydantic.dataclasses import dataclass as pydantic_dataclass
from pydantic import Field
@pydantic_dataclass
class Person:
"""Pydantic 검증 + dataclass 인터페이스"""
name: str
age: int = Field(ge=0)
# 검증 수행
try:
person = Person(name="Alice", age=-1) # ValidationError
except Exception as e:
print(e)
# 유효한 데이터
person = Person(name="Bob", age=30)
print(person)
7. 연습 문제¶
연습 1: 불변 설정 클래스¶
불변(frozen) 설정 클래스를 작성하세요.
# 예시 답안
from dataclasses import dataclass, field
from typing import Dict, Any
@dataclass(frozen=True)
class AppConfig:
app_name: str
version: str
debug: bool = False
settings: tuple = field(default_factory=tuple) # 불변 컬렉션
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "AppConfig":
return cls(
app_name=data["app_name"],
version=data["version"],
debug=data.get("debug", False),
settings=tuple(data.get("settings", [])),
)
config = AppConfig("MyApp", "1.0.0")
config_dict = {"app_name": "MyApp", "version": "1.0.0", "debug": True}
config2 = AppConfig.from_dict(config_dict)
연습 2: 자동 계산 필드¶
면적과 둘레를 자동 계산하는 Circle 클래스를 작성하세요.
# 예시 답안
from dataclasses import dataclass, field
import math
@dataclass
class Circle:
radius: float
area: float = field(init=False)
circumference: float = field(init=False)
def __post_init__(self):
if self.radius < 0:
raise ValueError("Radius cannot be negative")
self.area = math.pi * self.radius ** 2
self.circumference = 2 * math.pi * self.radius
circle = Circle(5)
print(f"Area: {circle.area:.2f}") # ~78.54
print(f"Circumference: {circle.circumference:.2f}") # ~31.42
연습 3: 중첩 데이터클래스와 JSON¶
중첩된 데이터클래스를 JSON으로 직렬화/역직렬화하세요.
# 예시 답안
from dataclasses import dataclass, asdict, field
from typing import List
import json
@dataclass
class OrderItem:
product_name: str
quantity: int
price: float
@dataclass
class Order:
order_id: str
customer: str
items: List[OrderItem] = field(default_factory=list)
def to_json(self) -> str:
return json.dumps(asdict(self), indent=2)
@classmethod
def from_json(cls, json_str: str) -> "Order":
data = json.loads(json_str)
items = [OrderItem(**item) for item in data.pop("items", [])]
return cls(**data, items=items)
order = Order(
"ORD001",
"Alice",
[OrderItem("Book", 2, 15.99), OrderItem("Pen", 5, 1.99)]
)
json_str = order.to_json()
print(json_str)
restored = Order.from_json(json_str)
print(restored)