@dataclass란?
@dataclass는 Python에서 데이터를 담는 class를 간단하게 작성할 수 있게 해주는 decorator이다.
일반적으로 class를 만들면 __init__, __repr__, __eq__ 같은 method를 직접 작성해야 한다.
하지만 @dataclass를 사용하면 type annotation을 기반으로 이런 method를 자동으로 만들어준다.
예를 들어 다음과 같은 class가 있다고 하자.
class User:
def __init__(self, id: int, name: str, email: str):
self.id = id
self.name = name
self.email = email
def __repr__(self):
return f"User(id={self.id!r}, name={self.name!r}, email={self.email!r})"
@dataclass를 사용하면 다음처럼 줄일 수 있다.
from dataclasses import dataclass
@dataclass
class User:
id: int
name: str
email: str
사용은 일반 class와 같다.
user = User(id=1, name="Alice", email="alice@example.com")
print(user)
출력은 다음과 비슷하다.
User(id=1, name='Alice', email='alice@example.com')
자동으로 만들어지는 method
@dataclass는 기본적으로 다음 method를 자동 생성한다.
| method | 역할 |
|---|---|
__init__ | field를 받아 instance 초기화 |
__repr__ | instance를 읽기 좋은 문자열로 표현 |
__eq__ | 두 instance의 field 값을 비교 |
예를 들어 다음 두 객체는 같은 값으로 비교된다.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p1 = Point(1, 2)
p2 = Point(1, 2)
print(p1 == p2) # True
일반 class였다면 __eq__를 직접 구현하지 않는 이상 두 객체는 서로 다른 객체로 비교된다.
dataclass는 field 값 기준 비교를 자동으로 제공한다.
기본값 설정
field에 기본값을 줄 수 있다.
from dataclasses import dataclass
@dataclass
class ServerConfig:
host: str = "localhost"
port: int = 8000
debug: bool = False
이제 인자를 생략할 수 있다.
config = ServerConfig()
print(config)
ServerConfig(host='localhost', port=8000, debug=False)
일부 값만 바꿀 수도 있다.
config = ServerConfig(port=3000)
주의할 점은 기본값이 없는 field가 기본값이 있는 field보다 먼저 와야 한다는 것이다.
@dataclass
class User:
id: int
name: str = "unknown"
다음처럼 기본값이 있는 field 뒤에 기본값이 없는 field를 두면 error가 발생한다.
@dataclass
class User:
name: str = "unknown"
id: int # error
mutable default와 default_factory
list, dict 같은 mutable object를 기본값으로 직접 넣으면 안 된다.
from dataclasses import dataclass
@dataclass
class Team:
members: list[str] = [] # bad
이런 방식은 여러 instance가 같은 list를 공유할 수 있으므로 위험하다.
dataclass에서는 이런 실수를 막기 위해 mutable default를 제한한다.
대신 field(default_factory=...)를 사용한다.
from dataclasses import dataclass, field
@dataclass
class Team:
name: str
members: list[str] = field(default_factory=list)
이제 instance마다 새로운 list가 만들어진다.
team_a = Team("A")
team_b = Team("B")
team_a.members.append("Alice")
print(team_a.members) # ['Alice']
print(team_b.members) # []
dict도 마찬가지로 처리한다.
from dataclasses import dataclass, field
@dataclass
class Cache:
values: dict[str, int] = field(default_factory=dict)
field 옵션
field()를 사용하면 field의 동작을 세밀하게 제어할 수 있다.
from dataclasses import dataclass, field
@dataclass
class User:
id: int
password: str = field(repr=False)
repr=False를 사용하면 print(user)를 했을 때 해당 field가 출력되지 않는다.
user = User(id=1, password="secret")
print(user)
User(id=1)
비교에서 제외할 수도 있다.
from dataclasses import dataclass, field
@dataclass
class Item:
name: str
cache_key: str = field(compare=False)
compare=False인 field는 == 비교에 사용되지 않는다.
frozen dataclass
frozen=True를 사용하면 instance를 불변 객체처럼 만들 수 있다.
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
x: int
y: int
값을 만든 뒤 field를 바꾸려고 하면 error가 발생한다.
point = Coordinate(1, 2)
point.x = 10 # error
설정값, 좌표, 식별자처럼 생성 후 바뀌면 안 되는 데이터에 유용하다.
@dataclass(frozen=True)
class DatabaseConfig:
host: str
port: int
database: str
다만 완전한 깊은 불변성을 보장하는 것은 아니다. field 안에 mutable object가 들어가면 그 내부 object는 여전히 변경될 수 있다.
order 옵션
order=True를 사용하면 크기 비교 method가 생성된다.
from dataclasses import dataclass
@dataclass(order=True)
class Student:
score: int
name: str
이제 정렬이 가능하다.
students = [
Student(90, "Alice"),
Student(80, "Bob"),
Student(95, "Charlie"),
]
students.sort()
print(students)
정렬은 field 선언 순서를 기준으로 한다.
위 예시는 score를 먼저 비교하고, score가 같으면 name을 비교한다.
특정 field를 비교에서 제외하려면 compare=False를 사용한다.
from dataclasses import dataclass, field
@dataclass(order=True)
class Student:
score: int
name: str = field(compare=False)
post_init
@dataclass가 생성한 __init__ 이후에 추가 초기화가 필요하면 __post_init__을 사용한다.
from dataclasses import dataclass
@dataclass
class Rectangle:
width: int
height: int
area: int = 0
def __post_init__(self):
self.area = self.width * self.height
사용 예시는 다음과 같다.
rect = Rectangle(3, 4)
print(rect.area) # 12
입력값 검증에도 사용할 수 있다.
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: int
def __post_init__(self):
if self.price < 0:
raise ValueError("price must be non-negative")
asdict와 astuple
dataclass instance를 dictionary나 tuple로 바꿀 수 있다.
from dataclasses import asdict, astuple, dataclass
@dataclass
class User:
id: int
name: str
user = User(1, "Alice")
print(asdict(user))
print(astuple(user))
출력은 다음과 같다.
{'id': 1, 'name': 'Alice'}
(1, 'Alice')
API response, config 변환, logging 등에 사용할 수 있다.
다만 JSON 문자열로 바로 바뀌는 것은 아니다.
JSON으로 변환하려면 json.dumps()와 함께 사용한다.
import json
from dataclasses import asdict, dataclass
@dataclass
class User:
id: int
name: str
user = User(1, "Alice")
payload = json.dumps(asdict(user))
dataclass가 적합한 경우
@dataclass는 데이터를 담는 목적의 class에 잘 맞는다.
| 상황 | 설명 |
|---|---|
| DTO | 계층 사이에서 데이터를 전달 |
| config | 설정값을 묶어서 관리 |
| value object | 좌표, 색상, 범위 같은 값 표현 |
| parser result | parsing 결과를 구조화 |
| test fixture | 테스트용 데이터를 간단히 생성 |
예를 들어 API 요청 결과를 담는 class로 사용할 수 있다.
from dataclasses import dataclass
@dataclass
class LoginResult:
user_id: int
access_token: str
expires_in: int
dataclass가 애매한 경우
@dataclass는 모든 class를 대체하는 도구가 아니다.
다음 상황에서는 일반 class가 더 적절할 수 있다.
| 상황 | 이유 |
|---|---|
| 복잡한 invariant | 생성과 변경 규칙을 강하게 통제해야 함 |
| behavior 중심 객체 | data보다 method가 핵심 |
| 상속 구조가 복잡함 | dataclass field 순서와 init 규칙이 복잡해짐 |
| validation이 많음 | Pydantic 같은 library가 더 적합할 수 있음 |
| private state가 중요함 | 단순 data container와 맞지 않을 수 있음 |
즉 @dataclass는 data container를 간결하게 만드는 도구로 보는 것이 좋다.
정리
@dataclass는 Python에서 데이터를 담는 class를 간단하게 만들기 위한 decorator이다.
핵심은 다음과 같다.
- type annotation을 기반으로
__init__,__repr__,__eq__를 자동 생성한다. - 기본값을 field에 직접 줄 수 있다.
- mutable default는
field(default_factory=...)를 사용한다. frozen=True로 불변 객체처럼 만들 수 있다.order=True로 정렬 가능한 객체를 만들 수 있다.- 추가 초기화와 검증은
__post_init__에서 처리한다. asdict,astuple로 다른 자료구조로 변환할 수 있다.
데이터를 묶기 위해 반복적인 class 코드를 작성하고 있다면 @dataclass를 먼저 고려해볼 만하다.