C.W.K.
Stream
Lesson 06 of 07 · published

dataclass — 보일러플레이트 제거 제대로

~22 min · dataclass, frozen, kw_only, field, post_init

Level 0호기심
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

약속 — 30 줄 대신 3 줄

대부분 단순 클래스 — __init__, __repr__, __eq__ 필요. 손으로 짜면 반복적. @dataclass 가 클래스 본문의 타입 힌트 읽고 다 생성. class Point: x: int; y: int — 선언 3 줄, 장황한 손으로 쓴 버전과 정확히 같이 동작.

실제로 중요한 데코레이터 파라미터

frozen=True — 인스턴스 immutable (생성 후 속성 할당 X, __hash__ 자동). kw_only=True — 모든 필드 생성 시 keyword-only (positional 인자 X). order=True — 정렬 dunder 생성. slots=True (3.10+) — 메모리 효율 위한 __slots__. 디폴트 (@dataclass 괄호 없이) 가 일회용 데이터 컨테이너에 보통 맞음.

field() — 단순 형태로 표현 못 하는 경우

필드 디폴트 값은 디폴트 인자처럼 작동 — 같은 mutable-default 함정. tags: list[str] = [] 가 인스턴스 간 list 공유. 해결 — tags: list[str] = field(default_factory=list). field() 는 또 __repr__ 에서 필드 skip, __eq__ 에서 제외, init=False 도 가능.

__post_init__ — 자동 init 후 추가 setup

dataclass 가 __init__ 생성, 근데 가끔 추가 setup 로직 필요 — 검증, 파생 필드 등. __post_init__ 정의하면 자동 생성 __init__ 끝에 실행. "반지름에서 면적 계산" 또는 "음수면 raise" 는 여기.

Pythonic Way: 값 모양 클래스의 디폴트로 @dataclass 손에 닿아 — 요청 body, 설정 레코드, 파싱된 이벤트. "이 필드 들고있기" 너머 의미있는 동작 있으면 일반 class. tuple 의미 (immutable, 인덱싱, unpacking) 가 특히 원하면 typing.NamedTuple.

Code

손으로 쓴 클래스 vs dataclass — 같은 동작·python
from dataclasses import dataclass

# 손으로
class PointManual:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"PointManual(x={self.x}, y={self.y})"

    def __eq__(self, other):
        return isinstance(other, PointManual) and self.x == other.x and self.y == other.y

# Dataclass
@dataclass
class Point:
    x: int
    y: int

p1 = Point(3, 4)
p2 = Point(3, 4)
print(p1)              # Point(x=3, y=4)
print(p1 == p2)        # True
frozen / kw_only / order — 유용한 플래그·python
from dataclasses import dataclass

@dataclass(frozen=True)
class Coordinate:
    lat: float
    lon: float

c = Coordinate(37.5, 127.0)
try:
    c.lat = 40.0          # frozen — 할당 raise
except Exception as e:
    print(type(e).__name__, e)

# frozen 은 무료로 __hash__ 도
print({c, Coordinate(37.5, 127.0)})    # 한 원소 set (dedup)

@dataclass(kw_only=True)
class Config:
    host: str
    port: int = 8000
    debug: bool = False

# 키워드 인자 필수
cfg = Config(host="localhost", debug=True)
try:
    Config("localhost", 9000)         # positional 거부
except TypeError as e:
    print(e)
field — 디폴트 / factory / 제외·python
from dataclasses import dataclass, field

@dataclass
class User:
    name: str
    tags: list[str] = field(default_factory=list)    # 인스턴스마다 새 list
    password_hash: str = field(repr=False, default="") # __repr__ 에서 제외
    id: int = field(init=False, default=0)            # __init__ 인자 X

u1 = User("alice")
u1.tags.append("admin")
u2 = User("bob")
print(u1.tags)         # ['admin']
print(u2.tags)         # []          — 독립
print(u1)              # User(name='alice', tags=['admin'], id=0)   — password_hash 숨김
__post_init__ — 파생 필드 + 검증·python
from dataclasses import dataclass, field

@dataclass
class Range:
    start: int
    end: int
    length: int = field(init=False)             # __init__ X, 계산

    def __post_init__(self):
        if self.end < self.start:
            raise ValueError("end >= start 여야")
        self.length = self.end - self.start

r = Range(start=10, end=25)
print(r)                  # Range(start=10, end=25, length=15)

try:
    Range(start=10, end=5)
except ValueError as e:
    print(e)
dataclass 안 쓸 때 — 진짜 동작 있을 때·python
# dataclass 는 *값* 용 (자동 __init__/__repr__/__eq__ 가진 데이터).
# 풍부한 동작 있으면 일반 클래스.

class Connection:               # 동작 많음 — 좋은 dataclass 후보 X
    def __init__(self, host):
        self.host = host
        self._socket = None

    def connect(self):
        # 복잡한 setup
        self._socket = f"<socket to {self.host}>"

    def send(self, data):
        if self._socket is None:
            raise RuntimeError("연결 X")
        return f"sent {data!r} via {self._socket}"

# 이거 dataclass 로 하려면 post_init + field(init=False) 잔뜩 — 직접 쓰는 게 명확.

External links

Exercise

@dataclass Order — 필드 id: str, items: list[str], total: float = 0.0. items 에 field(default_factory=list). __post_init__ 에서 가격 lookup dict 로 total 재계산 (힌트 — dataclass 의 클래스 레벨 상수로 가격 dict — PRICES: ClassVar[dict] = {...}). 그 다음 두 번째 변형 FrozenOrder(frozen=True, kw_only=True) — 불변성 + set 가능 보여.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.