Python Dataclasses: @dataclass 데코레이터 완전 가이드
Updated on
Python 클래스를 작성하다 보면 반복적인 보일러플레이트 코드가 자주 생깁니다. 속성을 초기화하기 위해 __init__를 정의하고, 보기 좋은 출력용 __repr__, 비교를 위한 __eq__, 그리고 경우에 따라 해시 가능성을 위한 __hash__까지 구현하게 됩니다. 이런 수동 구현은 설정 객체, API 응답, 데이터베이스 레코드처럼 “데이터를 담는” 클래스에서는 특히 번거롭습니다.
Python 3.7에서는 PEP 557을 통해 dataclasses가 도입되어, 일반 클래스의 유연성은 유지하면서도 이 보일러플레이트를 자동화할 수 있게 되었습니다. @dataclass 데코레이터는 타입 어노테이션을 기반으로 특수 메서드를 자동 생성해 수십 줄의 코드를 몇 줄로 줄여줍니다. 이 가이드는 dataclasses를 활용해 더 깔끔하고 유지보수하기 쉬운 Python 코드를 작성하는 방법을 보여줍니다.
Dataclasses가 존재하는 이유: 보일러플레이트 문제 해결
전통적인 Python 클래스는 흔히 사용하는 작업을 위해 메서드를 명시적으로 정의해야 합니다. 예를 들어 사용자 데이터를 저장하는 표준 클래스를 보면 다음과 같습니다.
class User:
def __init__(self, name, email, age):
self.name = name
self.email = email
self.age = age
def __repr__(self):
return f"User(name={self.name!r}, email={self.email!r}, age={self.age!r})"
def __eq__(self, other):
if not isinstance(other, User):
return NotImplemented
return (self.name, self.email, self.age) == (other.name, other.email, other.age)dataclasses를 사용하면 다음처럼 줄어듭니다.
from dataclasses import dataclass
@dataclass
class User:
name: str
email: str
age: int데코레이터는 타입 어노테이션을 바탕으로 __init__, __repr__, __eq__를 자동 생성합니다. 기능은 동일하게 유지하면서 15줄 이상의 보일러플레이트가 사라집니다.
기본 @dataclass 문법
가장 단순한 dataclass는 필드에 타입 어노테이션만 있으면 됩니다.
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
quantity: int
product = Product("Laptop", 999.99, 5)
print(product) # Product(name='Laptop', price=999.99, quantity=5)
product2 = Product("Laptop", 999.99, 5)
print(product == product2) # True데코레이터는 동작을 커스터마이즈하기 위한 파라미터도 받습니다.
@dataclass(
init=True, # __init__ 생성 (기본값: True)
repr=True, # __repr__ 생성 (기본값: True)
eq=True, # __eq__ 생성 (기본값: True)
order=False, # 비교 메서드 생성 (기본값: False)
frozen=False, # 인스턴스를 불변(immutable)으로 만들기 (기본값: False)
unsafe_hash=False # __hash__ 생성 (기본값: False)
)
class Config:
host: str
port: int필드 타입과 기본값
dataclasses는 필드 기본값을 지원합니다. 기본값이 없는 필드는 기본값이 있는 필드보다 앞에 와야 합니다.
from dataclasses import dataclass
@dataclass
class Server:
host: str
port: int = 8080
protocol: str = "http"
server1 = Server("localhost")
print(server1) # Server(host='localhost', port=8080, protocol='http')
server2 = Server("api.example.com", 443, "https")
print(server2) # Server(host='api.example.com', port=443, protocol='https')리스트/딕셔너리 같은 변경 가능한(mutable) 기본값은 공유 참조 문제를 피하기 위해 default_factory를 사용해야 합니다.
from dataclasses import dataclass, field
# WRONG - 모든 인스턴스가 같은 리스트를 공유
@dataclass
class WrongConfig:
tags: list = [] # Raises error in Python 3.10+
# CORRECT - 각 인스턴스가 새로운 리스트를 가짐
@dataclass
class CorrectConfig:
tags: list = field(default_factory=list)
metadata: dict = field(default_factory=dict)
config1 = CorrectConfig()
config2 = CorrectConfig()
config1.tags.append("production")
print(config1.tags) # ['production']
print(config2.tags) # [] - separate listfield() 함수: 고급 필드 설정
field() 함수는 개별 필드를 세밀하게 제어할 수 있게 해줍니다.
from dataclasses import dataclass, field
from typing import List
@dataclass
class Employee:
name: str
employee_id: int
salary: float = field(repr=False) # repr에서 salary 숨김
skills: List[str] = field(default_factory=list)
_internal_id: str = field(init=False, repr=False) # __init__에 포함되지 않음
performance_score: float = field(default=0.0, compare=False) # 비교에서 제외
def __post_init__(self):
self._internal_id = f"EMP_{self.employee_id:06d}"
emp = Employee("Alice", 12345, 85000.0, ["Python", "SQL"])
print(emp) # Employee(name='Alice', employee_id=12345, skills=['Python', 'SQL'], performance_score=0.0)
print(emp._internal_id) # EMP_012345주요 field() 파라미터:
| Parameter | Type | Description |
|---|---|---|
default | Any | 필드 기본값 |
default_factory | Callable | 기본값을 반환하는 인자 없는 함수 |
init | bool | __init__에 포함 (기본값: True) |
repr | bool | __repr__에 포함 (기본값: True) |
compare | bool | 비교 메서드에 포함 (기본값: True) |
hash | bool | __hash__에 포함 (기본값: None) |
metadata | dict | 임의 메타데이터(dataclasses 모듈이 사용하지는 않음) |
kw_only | bool | 필드를 keyword-only로 강제 (Python 3.10+) |
metadata 파라미터는 fields()를 통해 접근 가능한 임의 정보를 저장합니다.
from dataclasses import dataclass, field, fields
@dataclass
class APIRequest:
endpoint: str = field(metadata={"description": "API endpoint path"})
method: str = field(default="GET", metadata={"choices": ["GET", "POST", "PUT", "DELETE"]})
for f in fields(APIRequest):
print(f"{f.name}: {f.metadata}")
# endpoint: {'description': 'API endpoint path'}
# method: {'choices': ['GET', 'POST', 'PUT', 'DELETE']}Dataclasses에서의 타입 어노테이션
dataclasses는 타입 어노테이션을 활용하지만, 런타임에 타입을 강제하진 않습니다. 복잡한 타입은 typing 모듈을 사용하세요.
from dataclasses import dataclass
from typing import List, Dict, Optional, Union, Tuple
from datetime import datetime
@dataclass
class DataAnalysisJob:
job_id: str
dataset_path: str
columns: List[str]
filters: Dict[str, Union[str, int, float]]
output_format: str = "csv"
created_at: datetime = field(default_factory=datetime.now)
completed_at: Optional[datetime] = None
error_message: Optional[str] = None
results: Optional[Dict[str, Tuple[float, float]]] = None
job = DataAnalysisJob(
job_id="job_001",
dataset_path="/data/sales.csv",
columns=["date", "revenue", "region"],
filters={"year": 2026, "region": "US"}
)런타임 타입 체크가 필요하다면 pydantic 같은 라이브러리를 통합하거나 __post_init__에서 검증 로직을 추가하세요.
frozen=True: 불변(immutable) Dataclass 만들기
frozen=True를 설정하면 생성 이후 인스턴스를 수정할 수 없게 되어 named tuple과 비슷한 성질을 갖습니다.
from dataclasses import dataclass
@dataclass(frozen=True)
class Point:
x: float
y: float
def distance_from_origin(self):
return (self.x**2 + self.y**2) ** 0.5
point = Point(3.0, 4.0)
print(point.distance_from_origin()) # 5.0
# Attempting to modify raises FrozenInstanceError
try:
point.x = 5.0
except AttributeError as e:
print(f"Error: {e}") # Error: cannot assign to field 'x'Frozen dataclass는 모든 필드가 hashable이면 기본적으로 hashable이 되어, set이나 dict 키로도 사용할 수 있습니다.
@dataclass(frozen=True)
class Coordinate:
latitude: float
longitude: float
locations = {
Coordinate(40.7128, -74.0060): "New York",
Coordinate(51.5074, -0.1278): "London"
}
print(locations[Coordinate(40.7128, -74.0060)]) # New Yorkpost_init 메서드: 검증과 계산 필드
__post_init__는 __init__ 이후에 실행되며, 검증이나 계산된 필드 초기화에 사용할 수 있습니다.
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class BankAccount:
account_number: str
balance: float
created_at: datetime = field(default_factory=datetime.now)
account_type: str = field(init=False)
def __post_init__(self):
if self.balance < 0:
raise ValueError("Initial balance cannot be negative")
# Compute account_type based on balance
if self.balance >= 100000:
self.account_type = "Premium"
elif self.balance >= 10000:
self.account_type = "Gold"
else:
self.account_type = "Standard"
account = BankAccount("ACC123456", 50000.0)
print(account.account_type) # Gold다른 필드에 의존하는 init=False 필드는 __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.0, 3.0)
print(f"Area: {rect.area}, Perimeter: {rect.perimeter}") # Area: 15.0, Perimeter: 16.0Dataclasses에서의 상속
dataclasses는 필드를 자동으로 병합하는 방식으로 상속을 지원합니다.
from dataclasses import dataclass
@dataclass
class Animal:
name: str
age: int
@dataclass
class Dog(Animal):
breed: str
is_good_boy: bool = True
dog = Dog("Buddy", 5, "Golden Retriever")
print(dog) # Dog(name='Buddy', age=5, breed='Golden Retriever', is_good_boy=True)서브클래스는 부모 필드를 상속받고 새 필드를 추가할 수 있습니다. 단, 상속 관계 전체에서 “기본값 없는 필드가 기본값 있는 필드 뒤에 올 수 없다”는 규칙은 동일하게 적용됩니다.
from dataclasses import dataclass
@dataclass
class BaseConfig:
environment: str = "production"
# ERROR: Non-default field 'api_key' cannot follow default field 'environment'
# @dataclass
# class APIConfig(BaseConfig):
# api_key: str
# CORRECT: 기본값을 주거나 필드 순서를 조정
@dataclass
class APIConfig(BaseConfig):
api_key: str = "" # 기본값 제공
timeout: int = 30Python 3.10+에서는 kw_only로 이 문제를 해결할 수 있습니다.
from dataclasses import dataclass
@dataclass
class BaseConfig:
environment: str = "production"
@dataclass(kw_only=True)
class APIConfig(BaseConfig):
api_key: str # keyword argument로만 전달 가능
timeout: int = 30
config = APIConfig(api_key="secret_key_123") # OK
# config = APIConfig("secret_key_123") # TypeErrorslots=True: 메모리 효율 (Python 3.10+)
Python 3.10에서 slots=True가 추가되어 __slots__를 정의할 수 있고, 메모리 오버헤드를 줄일 수 있습니다.
from dataclasses import dataclass
import sys
@dataclass
class RegularUser:
username: str
email: str
age: int
@dataclass(slots=True)
class SlottedUser:
username: str
email: str
age: int
regular = RegularUser("john", "john@example.com", 30)
slotted = SlottedUser("jane", "jane@example.com", 28)
print(f"Regular: {sys.getsizeof(regular.__dict__)} bytes") # ~104 bytes
print(f"Slotted: {sys.getsizeof(slotted)} bytes") # ~64 bytesslots를 사용하는 dataclass는 30~40% 메모리 절감과 더 빠른 속성 접근을 제공하지만, 동적 속성 추가는 포기해야 합니다.
regular.new_attribute = "allowed" # OK
# slotted.new_attribute = "error" # AttributeErrorkw_only=True: Keyword-only 필드 (Python 3.10+)
모든 필드를 keyword-only로 강제하면 인스턴스 생성이 더 명확해집니다.
from dataclasses import dataclass
@dataclass(kw_only=True)
class DatabaseConnection:
host: str
port: int
username: str
password: str
database: str = "default"
# keyword arguments를 사용해야 함
conn = DatabaseConnection(
host="localhost",
port=5432,
username="admin",
password="secret"
)
# 위치 인자는 TypeError 발생
# conn = DatabaseConnection("localhost", 5432, "admin", "secret")kw_only를 필드 단위로 섞어 쓸 수도 있습니다.
from dataclasses import dataclass, field
@dataclass
class MixedArgs:
required_positional: str
optional_positional: int = 0
required_keyword: str = field(kw_only=True)
optional_keyword: bool = field(default=False, kw_only=True)
obj = MixedArgs("value", 10, required_keyword="kw_value")비교: dataclass vs 대안들
| Feature | dataclass | namedtuple | TypedDict | Pydantic | attrs |
|---|---|---|---|---|---|
| Mutability | 기본 Mutable | Immutable | N/A (dict subclass) | Mutable | 설정 가능 |
| Type validation | 어노테이션만 | 없음 | 어노테이션만 | 런타임 검증 | 런타임 검증 |
| Default values | 가능 | 가능 | 불가 | 가능 | 가능 |
| Methods | 완전한 class 지원 | 제한적 | 없음 | 완전한 class 지원 | 완전한 class 지원 |
| Inheritance | 가능 | 불가 | 제한적 | 가능 | 가능 |
| Memory overhead | 중간 | 낮음 | 낮음 | 높음 | 중간 |
| Slots support | 가능 (3.10+) | 불가 | 불가 | 가능 | 가능 |
| Performance | 빠름 | 가장 빠름 | 빠름 | 느림(검증) | 빠름 |
| Built-in | 예 (3.7+) | 예 | 예 (3.8+) | 아니오 | 아니오 |
dataclasses를 선택하면 좋은 경우:
- 의존성 없는 일반 Python 프로젝트
- 타입 힌트가 있는 단순 데이터 컨테이너
- frozen/mutable 유연성이 필요할 때
- 상속 계층이 있는 모델
Pydantic을 선택하면 좋은 경우:
- API 요청/응답 검증
- 엄격한 검증이 필요한 설정 관리
- JSON schema 생성
namedtuple을 선택하면 좋은 경우:
- 가벼운 불변 컨테이너
- 최대 메모리 효율
- Python < 3.7 호환성이 필요할 때
딕셔너리로/딕셔너리에서 변환
dataclasses는 직렬화를 위해 asdict()와 astuple()을 제공합니다.
from dataclasses import dataclass, asdict, astuple
@dataclass
class Config:
host: str
port: int
ssl_enabled: bool = True
config = Config("api.example.com", 443)
# 딕셔너리로 변환
config_dict = asdict(config)
print(config_dict) # {'host': 'api.example.com', 'port': 443, 'ssl_enabled': True}
# 튜플로 변환
config_tuple = astuple(config)
print(config_tuple) # ('api.example.com', 443, True)중첩 dataclass에도 동작합니다.
from dataclasses import dataclass, asdict
@dataclass
class Address:
street: str
city: str
zipcode: str
@dataclass
class Person:
name: str
address: Address
person = Person("Alice", Address("123 Main St", "Springfield", "12345"))
person_dict = asdict(person)
print(person_dict)
# {'name': 'Alice', 'address': {'street': '123 Main St', 'city': 'Springfield', 'zipcode': '12345'}}Dataclasses와 JSON 직렬화
dataclasses는 JSON 직렬화를 네이티브로 제공하진 않지만, 연동은 간단합니다.
import json
from dataclasses import dataclass, asdict
from datetime import datetime
@dataclass
class Event:
name: str
timestamp: datetime
attendees: int
def to_json(self):
data = asdict(self)
# datetime에 대한 커스텀 직렬화
data['timestamp'] = self.timestamp.isoformat()
return json.dumps(data)
@classmethod
def from_json(cls, json_str):
data = json.loads(json_str)
data['timestamp'] = datetime.fromisoformat(data['timestamp'])
return cls(**data)
event = Event("Python Conference", datetime.now(), 500)
json_str = event.to_json()
print(json_str)
restored = Event.from_json(json_str)
print(restored)더 복잡한 시나리오에서는 dataclasses-json 라이브러리나 Pydantic을 사용하세요.
실전 패턴
설정 객체(Configuration Objects)
from dataclasses import dataclass, field
from typing import List
@dataclass
class AppConfig:
app_name: str
version: str
debug: bool = False
allowed_hosts: List[str] = field(default_factory=lambda: ["localhost"])
database_url: str = "sqlite:///app.db"
cache_timeout: int = 300
def __post_init__(self):
if self.debug:
print(f"Running {self.app_name} v{self.version} in DEBUG mode")
config = AppConfig("DataAnalyzer", "2.1.0", debug=True)API 응답 모델
from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime
@dataclass
class APIResponse:
status: str
data: Optional[List[dict]] = None
error_message: Optional[str] = None
timestamp: datetime = field(default_factory=datetime.now)
@property
def is_success(self):
return self.status == "success"
response = APIResponse("success", data=[{"id": 1, "name": "Dataset A"}])
print(response.is_success) # TruePyGWalker 통합을 활용한 데이터베이스 레코드
from dataclasses import dataclass, asdict
from typing import List
import pandas as pd
@dataclass
class SalesRecord:
date: str
product: str
revenue: float
region: str
quantity: int
# Create sample data
records = [
SalesRecord("2026-01-01", "Laptop", 1299.99, "US", 5),
SalesRecord("2026-01-02", "Mouse", 29.99, "EU", 50),
SalesRecord("2026-01-03", "Keyboard", 89.99, "US", 20),
]
# Convert to DataFrame for visualization with PyGWalker
df = pd.DataFrame([asdict(r) for r in records])
# Use PyGWalker for interactive data exploration
# import pygwalker as pyg
# walker = pyg.walk(df)
# This creates a Tableau-like interface to visualize your dataclass-based datadataclasses는 시각화 이전 단계에서 데이터를 구조화하는 데 매우 유용합니다. PyGWalker는 DataFrame을 인터랙티브한 시각 인터페이스로 변환해 주므로, dataclass 기반 데이터 분석 워크플로우를 매끄럽게 이어줍니다.
일반 클래스 대비 성능 벤치마크
import timeit
from dataclasses import dataclass
# Regular class
class RegularClass:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def __repr__(self):
return f"RegularClass(x={self.x}, y={self.y}, z={self.z})"
def __eq__(self, other):
return (self.x, self.y, self.z) == (other.x, other.y, other.z)
@dataclass
class DataClass:
x: int
y: int
z: int
# Benchmark instantiation
regular_time = timeit.timeit(lambda: RegularClass(1, 2, 3), number=1000000)
dataclass_time = timeit.timeit(lambda: DataClass(1, 2, 3), number=1000000)
print(f"Regular class: {regular_time:.4f}s")
print(f"Dataclass: {dataclass_time:.4f}s")
# Dataclasses are typically 5-10% slower due to decorator overhead
# but provide significantly cleaner codeslots=True(Python 3.10+)를 사용하면 dataclass는 일반 클래스의 성능에 근접하거나 앞서면서, 메모리 사용량도 30~40% 줄일 수 있습니다.
고급 패턴: 커스텀 필드 정렬
from dataclasses import dataclass, field
def sort_by_priority(items):
return sorted(items, key=lambda x: x.priority, reverse=True)
@dataclass(order=True)
class Task:
priority: int
name: str = field(compare=False)
description: str = field(compare=False)
tasks = [
Task(3, "Review PR", "Code review for feature X"),
Task(1, "Write docs", "Documentation update"),
Task(5, "Fix bug", "Critical production issue"),
]
sorted_tasks = sorted(tasks)
for task in sorted_tasks:
print(f"Priority {task.priority}: {task.name}")
# Priority 1: Write docs
# Priority 3: Review PR
# Priority 5: Fix bug모범 사례와 주의사항
- mutable 기본값에는 항상
default_factory를 사용:[],{}를 직접 할당하지 말 것 - 타입 힌트는 필수: dataclass는 값이 아니라 어노테이션에 의존
- 필드 순서가 중요: 기본값 없는 필드를 앞에 배치
- 불변 데이터에는
frozen=True사용: hashable 객체, 스레드 안전성에 유리 __post_init__는 최소한으로: 로직이 과하면 dataclass의 단순함이 무너짐- 대규모 데이터에는
slots=True고려: Python 3.10+에서 큰 메모리 절감 __post_init__에서 검증 수행: dataclass는 런타임 타입을 강제하지 않음
FAQ
결론
Python dataclasses는 클래스의 모든 장점을 유지하면서 보일러플레이트 코드를 제거합니다. @dataclass 데코레이터는 초기화, 표현, 비교 메서드를 자동으로 생성해 개발 시간과 유지보수 부담을 줄여줍니다. 설정 객체부터 API 모델, 데이터베이스 레코드에 이르기까지 dataclasses는 타입 어노테이션 기반으로 데이터를 담는 클래스를 깔끔하게 설계하는 방법을 제공합니다.
핵심 장점은 자동 메서드 생성, field()를 통한 필드 동작 커스터마이즈, frozen=True로 불변성 제공, __post_init__로 검증/계산 처리, slots=True로 메모리 효율 개선입니다. namedtuple이나 Pydantic 같은 대안도 특정 목적에 유리하지만, dataclasses는 대부분의 Python 프로젝트에서 단순성과 기능성 사이의 균형이 뛰어납니다.
데이터 분석 워크플로우에서는 dataclasses를 PyGWalker 같은 도구와 결합해, 구조화된 데이터 모델을 인터랙티브 시각화로 바로 연결하는 강력한 파이프라인을 만들 수 있습니다. 이를 통해 데이터 수집부터 인사이트 도출까지의 흐름을 크게 단순화할 수 있습니다.