Skip to content

Latest commit

Β 

History

History
951 lines (740 loc) Β· 22.9 KB

File metadata and controls

951 lines (740 loc) Β· 22.9 KB

Python @dataclass

Java의 Lombok @Dataλ‚˜ record처럼, λ³΄μΌλŸ¬ν”Œλ ˆμ΄νŠΈ 없이 데이터 클래슀λ₯Ό λ§Œλ“œλŠ” 방법.

κ²°λ‘ λΆ€ν„° λ§ν•˜λ©΄

@dataclassλŠ” 데이터λ₯Ό μ €μž₯ν•˜λŠ” 클래슀λ₯Ό κ°„νŽΈν•˜κ²Œ λ§Œλ“€μ–΄μ£ΌλŠ” λ°μ½”λ ˆμ΄ν„°λ‘œ, 반볡적인 λ³΄μΌλŸ¬ν”Œλ ˆμ΄νŠΈ μ½”λ“œλ₯Ό μžλ™μœΌλ‘œ μƒμ„±ν•΄μ€λ‹ˆλ‹€.

from dataclasses import dataclass

# βœ… dataclass μ‚¬μš© (5쀄)
@dataclass
class Person:
    name: str
    age: int
    email: str

# ❌ 일반 클래슀 (15쀄+)
class PersonOld:
    def __init__(self, name: str, age: int, email: str):
        self.name = name
        self.age = age
        self.email = email

    def __repr__(self):
        return f"PersonOld(name={self.name!r}, age={self.age!r}, email={self.email!r})"

    def __eq__(self, other):
        if not isinstance(other, PersonOld):
            return NotImplemented
        return (self.name, self.age, self.email) == (other.name, other.age, other.email)

1. κΈ°λ³Έ μ‚¬μš©λ²•

κ°„λ‹¨ν•œ 데이터 클래슀

from dataclasses import dataclass

@dataclass
class User:
    username: str
    email: str
    age: int

# μΈμŠ€ν„΄μŠ€ 생성
user = User("hongildong", "hong@example.com", 30)

print(user)
# User(username='hongildong', email='hong@example.com', age=30)

print(user.username)  # "hongildong"
print(user.age)       # 30

μžλ™ μƒμ„±λ˜λŠ” λ©”μ„œλ“œ

@dataclassλŠ” λ‹€μŒ λ©”μ„œλ“œλ“€μ„ μžλ™μœΌλ‘œ μƒμ„±ν•©λ‹ˆλ‹€:

  • __init__: μƒμ„±μž
  • __repr__: λ¬Έμžμ—΄ ν‘œν˜„
  • __eq__: 동등 비ꡐ
@dataclass
class Point:
    x: int
    y: int

p1 = Point(1, 2)
p2 = Point(1, 2)
p3 = Point(3, 4)

# __repr__ μžλ™ 생성
print(p1)  # Point(x=1, y=2)

# __eq__ μžλ™ 생성
print(p1 == p2)  # True
print(p1 == p3)  # False

2. κΈ°λ³Έκ°’ μ„€μ •

일반 κΈ°λ³Έκ°’

@dataclass
class Product:
    name: str
    price: float
    stock: int = 0          # κΈ°λ³Έκ°’
    category: str = "기타"   # κΈ°λ³Έκ°’

p1 = Product("λ…ΈνŠΈλΆ", 1200000)
print(p1)
# Product(name='λ…ΈνŠΈλΆ', price=1200000, stock=0, category='기타')

p2 = Product("마우슀", 35000, stock=150, category="μ£Όλ³€κΈ°κΈ°")
print(p2)
# Product(name='마우슀', price=35000, stock=150, category='μ£Όλ³€κΈ°κΈ°')

주의: κΈ°λ³Έκ°’ μˆœμ„œ

# ❌ 였λ₯˜ λ°œμƒ
@dataclass
class Wrong:
    name: str = "default"
    age: int  # κΈ°λ³Έκ°’ μ—†λŠ” ν•„λ“œκ°€ 뒀에 였면 μ•ˆ 됨!
# TypeError!

# βœ… μ˜¬λ°”λ₯Έ μˆœμ„œ
@dataclass
class Correct:
    age: int                   # κΈ°λ³Έκ°’ μ—†λŠ” ν•„λ“œκ°€ λ¨Όμ €
    name: str = "default"      # κΈ°λ³Έκ°’ μžˆλŠ” ν•„λ“œκ°€ λ‚˜μ€‘

3. κ°€λ³€ κΈ°λ³Έκ°’ (field μ‚¬μš©)

문제 상황

# ❌ μœ„ν—˜! κ°€λ³€ 객체λ₯Ό κΈ°λ³Έκ°’μœΌλ‘œ μ‚¬μš©ν•˜λ©΄ μ•ˆ 됨
@dataclass
class Team:
    name: str
    members: list = []  # ValueError λ°œμƒ!
# ValueError: mutable default <class 'list'> for field members is not allowed

ν•΄κ²°: field(default_factory)

from dataclasses import dataclass, field

@dataclass
class Team:
    name: str
    members: list = field(default_factory=list)  # 맀번 μƒˆ 리슀트 생성

team1 = Team("κ°œλ°œνŒ€")
team2 = Team("λ””μžμΈνŒ€")

team1.members.append("홍길동")
team2.members.append("κΉ€μ² μˆ˜")

print(team1.members)  # ["홍길동"]
print(team2.members)  # ["κΉ€μ² μˆ˜"] - 독립적!

λ‹€μ–‘ν•œ default_factory

from dataclasses import dataclass, field
from typing import Dict, Set
from datetime import datetime

@dataclass
class UserProfile:
    username: str
    tags: list = field(default_factory=list)
    metadata: dict = field(default_factory=dict)
    roles: set = field(default_factory=set)
    created_at: datetime = field(default_factory=datetime.now)

profile = UserProfile("hong")
print(profile.tags)        # []
print(profile.metadata)    # {}
print(profile.roles)       # set()
print(profile.created_at)  # ν˜„μž¬ μ‹œκ°„

4. dataclass μ˜΅μ…˜

μ£Όμš” νŒŒλΌλ―Έν„°

@dataclass(
    init=True,         # __init__ 생성 (κΈ°λ³Έ: True)
    repr=True,         # __repr__ 생성 (κΈ°λ³Έ: True)
    eq=True,           # __eq__ 생성 (κΈ°λ³Έ: True)
    order=False,       # __lt__, __le__, __gt__, __ge__ 생성 (κΈ°λ³Έ: False)
    frozen=False,      # λΆˆλ³€ 객체둜 λ§Œλ“€κΈ° (κΈ°λ³Έ: False)
    unsafe_hash=False  # κ°•μ œλ‘œ __hash__ 생성 (μžμ„Έν•œ κ·œμΉ™μ€ μ•„λž˜ μ°Έκ³ , κΈ°λ³Έ: False)
)
class Example:
    value: int

__hash__ μžλ™ 생성 κ·œμΉ™ (ν—·κ°ˆλ¦¬κΈ° 쉬움): unsafe_hash=False라고 항상 __hash__이 λΉ μ§€λŠ” 건 μ•„λ‹ˆλ‹€. dataclassλŠ” eq/frozen 쑰합에 따라 λ‹€μŒκ³Ό 같이 λ™μž‘ν•œλ‹€:

eq frozen unsafe_hash __hash__ κ²°κ³Ό
True (κΈ°λ³Έ) True False μžλ™ 생성 βœ… β€” set/dict ν‚€ μ‚¬μš© κ°€λŠ₯
True (κΈ°λ³Έ) False (κΈ°λ³Έ) False None으둜 μ„€μ • ❌ β€” hashable μ•„λ‹˜
True False True κ°•μ œ 생성 ⚠️ β€” mutable인데 hashκ°€ μžˆμ–΄ μœ„ν—˜
False 무관 False λΆ€λͺ¨ 클래슀의 __hash__ 상속

즉 unsafe_hash=TrueλŠ” "mutableν•œ dataclass에 κ°•μ œλ‘œ hashλ₯Ό λ§Œλ“€ λ•Œ"만 μ˜λ―Έκ°€ 있고, μΌλ°˜μ μœΌλ‘œλŠ” frozen=True둜 hashλ₯Ό μ–»λŠ” 것이 μ•ˆμ „ν•˜λ‹€.

frozen=True (λΆˆλ³€ 객체)

@dataclass(frozen=True)
class Point:
    x: int
    y: int

p = Point(1, 2)

# ❌ κ°’ λ³€κ²½ λΆˆκ°€
try:
    p.x = 10
except Exception as e:
    print(e)  # FrozenInstanceError: cannot assign to field 'x'

# βœ… dictionary ν‚€λ‘œ μ‚¬μš© κ°€λŠ₯
points = {Point(1, 2): "origin", Point(3, 4): "other"}
print(points[Point(1, 2)])  # "origin"

order=True (μ •λ ¬ κ°€λŠ₯)

@dataclass(order=True)
class Student:
    name: str
    score: int

students = [
    Student("홍길동", 85),
    Student("κΉ€μ² μˆ˜", 92),
    Student("이영희", 78)
]

# ν•„λ“œ μˆœμ„œλŒ€λ‘œ 비ꡐ (name -> score)
sorted_students = sorted(students)
for s in sorted_students:
    print(s)
# Student(name='κΉ€μ² μˆ˜', score=92)
# Student(name='이영희', score=78)
# Student(name='홍길동', score=85)

5. field μ˜΅μ…˜

repr=False (λ―Όκ°ν•œ 정보 숨기기)

from dataclasses import dataclass, field

@dataclass
class User:
    username: str
    email: str
    password: str = field(repr=False)  # __repr__μ—μ„œ μˆ¨κΉ€

user = User("hong", "hong@example.com", "secret123")
print(user)
# User(username='hong', email='hong@example.com')
# passwordλŠ” 좜λ ₯ μ•ˆ 됨!

init=False (μ΄ˆκΈ°ν™” ν›„ μ„€μ •)

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)  # __init__에 포함 μ•ˆ 함

    def __post_init__(self):
        self.area = self.width * self.height

rect = Rectangle(10, 5)
# Rectangle(width=10, height=5) ν˜•νƒœλ‘œλ§Œ 생성 κ°€λŠ₯
print(rect.area)  # 50.0

compare=False (λΉ„κ΅μ—μ„œ μ œμ™Έ)

@dataclass
class User:
    user_id: int
    username: str
    last_login: str = field(compare=False)  # 비ꡐ μ‹œ λ¬΄μ‹œ

user1 = User(1, "hong", "2025-01-01")
user2 = User(1, "hong", "2025-01-15")

print(user1 == user2)  # True (last_login은 비ꡐ μ•ˆ 함)

6. post_init (μ΄ˆκΈ°ν™” ν›„ 처리)

κ³„μ‚°λœ ν•„λ“œ

@dataclass
class Circle:
    radius: float
    area: float = field(init=False)
    circumference: float = field(init=False)

    def __post_init__(self):
        self.area = 3.14159 * self.radius ** 2
        self.circumference = 2 * 3.14159 * self.radius

circle = Circle(5)
print(f"λ°˜μ§€λ¦„: {circle.radius}")        # λ°˜μ§€λ¦„: 5
print(f"넓이: {circle.area:.2f}")        # 넓이: 78.54
print(f"λ‘˜λ ˆ: {circle.circumference:.2f}")  # λ‘˜λ ˆ: 31.42

κ°’ 검증

@dataclass
class User:
    username: str
    age: int

    def __post_init__(self):
        if self.age < 0:
            raise ValueError("λ‚˜μ΄λŠ” 0보닀 μž‘μ„ 수 μ—†μŠ΅λ‹ˆλ‹€")
        if len(self.username) < 3:
            raise ValueError("μ‚¬μš©μžλͺ…은 μ΅œμ†Œ 3자 이상이어야 ν•©λ‹ˆλ‹€")

# βœ… 정상
user1 = User("hongildong", 30)

# ❌ 였λ₯˜ λ°œμƒ
try:
    user2 = User("ho", 30)
except ValueError as e:
    print(e)  # μ‚¬μš©μžλͺ…은 μ΅œμ†Œ 3자 이상이어야 ν•©λ‹ˆλ‹€

try:
    user3 = User("hong", -5)
except ValueError as e:
    print(e)  # λ‚˜μ΄λŠ” 0보닀 μž‘μ„ 수 μ—†μŠ΅λ‹ˆλ‹€

νƒ€μž… λ³€ν™˜

@dataclass
class Product:
    name: str
    price: float

    def __post_init__(self):
        # λ¬Έμžμ—΄λ‘œ λ“€μ–΄μ˜¨ priceλ₯Ό float둜 λ³€ν™˜
        if isinstance(self.price, str):
            self.price = float(self.price)

p = Product("λ…ΈνŠΈλΆ", "1200000")
print(p.price)        # 1200000.0 (float둜 λ³€ν™˜λ¨)
print(type(p.price))  # <class 'float'>

7. 상속

κΈ°λ³Έ 상속

@dataclass
class Person:
    name: str
    age: int

@dataclass
class Employee(Person):
    employee_id: int
    department: str

emp = Employee("홍길동", 30, 12345, "κ°œλ°œνŒ€")
print(emp)
# Employee(name='홍길동', age=30, employee_id=12345, department='κ°œλ°œνŒ€')

ν•„λ“œ μˆœμ„œ 주의

# ❌ 였λ₯˜ λ°œμƒ
@dataclass
class Parent:
    name: str
    age: int = 0  # κΈ°λ³Έκ°’ 있음

@dataclass
class Child(Parent):
    student_id: int  # κΈ°λ³Έκ°’ μ—†λŠ” ν•„λ“œκ°€ 뒀에 였면 μ•ˆ 됨!
# TypeError!

# βœ… μ˜¬λ°”λ₯Έ 방법 1: μžμ‹ ν•„λ“œλ„ λͺ¨λ‘ κΈ°λ³Έκ°’ λΆ€μ—¬
@dataclass
class ChildCorrect(Parent):
    student_id: int = 0      # λΆ€λͺ¨(Parent.age=0)에 기본값이 μžˆμœΌλ―€λ‘œ μžμ‹ ν•„λ“œλ„ κΈ°λ³Έκ°’ ν•„μš”
    grade: str = "A"

# βœ… μ˜¬λ°”λ₯Έ 방법 2: kw_only둜 μœ„μΉ˜ 인자 μ œμ•½ νšŒν”Ό (Python 3.10+)
@dataclass(kw_only=True)
class ChildKwOnly(Parent):
    student_id: int          # ν‚€μ›Œλ“œ μ „μš©μ΄λΌ μœ„μΉ˜ 좩돌 μ—†μŒ
    grade: str = "A"

상속 μ‹œ 핡심 κ·œμΉ™: dataclassλŠ” λΆ€λͺ¨ β†’ μžμ‹ μˆœμ„œλ‘œ ν•„λ“œλ₯Ό ν•©μ³μ„œ __init__을 λ§Œλ“€κΈ° λ•Œλ¬Έμ—, 합쳐진 μ‹œκ·Έλ‹ˆμ²˜μ—μ„œλ„ "κΈ°λ³Έκ°’ μžˆλŠ” 인자 뒀에 κΈ°λ³Έκ°’ μ—†λŠ” 인자"κ°€ 였면 μ•ˆ λœλ‹€. Python 3.10+μ—μ„œλŠ” kw_only=Trueλ‚˜ field(kw_only=True)둜 이 μ œμ•½μ„ μš°νšŒν•  수 μžˆλ‹€.

8. asdict와 astuple

λ”•μ…”λ„ˆλ¦¬λ‘œ λ³€ν™˜

from dataclasses import dataclass, asdict

@dataclass
class User:
    username: str
    email: str
    age: int

user = User("hong", "hong@example.com", 30)

# λ”•μ…”λ„ˆλ¦¬λ‘œ λ³€ν™˜
user_dict = asdict(user)
print(user_dict)
# {'username': 'hong', 'email': 'hong@example.com', 'age': 30}

# JSON 직렬화에 유용
import json
json_str = json.dumps(user_dict)
print(json_str)
# {"username": "hong", "email": "hong@example.com", "age": 30}

νŠœν”Œλ‘œ λ³€ν™˜

from dataclasses import astuple

user = User("hong", "hong@example.com", 30)

# νŠœν”Œλ‘œ λ³€ν™˜
user_tuple = astuple(user)
print(user_tuple)
# ('hong', 'hong@example.com', 30)

# μ–ΈνŒ¨ν‚Ή
username, email, age = astuple(user)
print(username)  # hong
print(email)     # hong@example.com
print(age)       # 30

9. μ‹€μ „ μ˜ˆμ‹œ

API 응닡 λͺ¨λΈ

from dataclasses import dataclass, field, asdict
from typing import List, Optional
from datetime import datetime

@dataclass
class Comment:
    id: int
    author: str
    content: str
    created_at: datetime = field(default_factory=datetime.now)

@dataclass
class Post:
    id: int
    title: str
    content: str
    author: str
    tags: List[str] = field(default_factory=list)
    comments: List[Comment] = field(default_factory=list)
    views: int = 0
    created_at: datetime = field(default_factory=datetime.now)

# μ‚¬μš©
post = Post(
    id=1,
    title="Python dataclass μ™„μ „ 정볡",
    content="dataclassλŠ” 데이터 클래슀λ₯Ό μ‰½κ²Œ λ§Œλ“€μ–΄μ€λ‹ˆλ‹€.",
    author="홍길동",
    tags=["python", "tutorial"]
)

post.comments.append(Comment(1, "κΉ€μ² μˆ˜", "쒋은 κΈ€ κ°μ‚¬ν•©λ‹ˆλ‹€!"))
post.views += 1

print(post)

# JSON으둜 λ³€ν™˜
post_dict = asdict(post)

μ„€μ • 관리

from dataclasses import dataclass, field
import os

@dataclass
class DatabaseConfig:
    host: str = "localhost"
    port: int = 5432
    username: str = "postgres"
    password: str = field(default="", repr=False)  # λΉ„λ°€λ²ˆν˜Έ μˆ¨κΉ€
    database: str = "mydb"

    @classmethod
    def from_env(cls):
        """ν™˜κ²½ λ³€μˆ˜μ—μ„œ μ„€μ • λ‘œλ“œ"""
        return cls(
            host=os.getenv("DB_HOST", "localhost"),
            port=int(os.getenv("DB_PORT", "5432")),
            username=os.getenv("DB_USERNAME", "postgres"),
            password=os.getenv("DB_PASSWORD", ""),
            database=os.getenv("DB_DATABASE", "mydb")
        )

    def get_connection_string(self):
        return f"postgresql://{self.username}:{self.password}@{self.host}:{self.port}/{self.database}"

@dataclass
class AppConfig:
    secret_key: str = field(repr=False)   # κΈ°λ³Έκ°’ μ—†λŠ” ν•„λ“œ λ¨Όμ €
    debug: bool = False
    port: int = 8000
    database: DatabaseConfig = field(default_factory=DatabaseConfig)

# μ‚¬μš©
config = AppConfig(
    debug=True,
    port=8080,
    secret_key="my-secret-key",
    database=DatabaseConfig.from_env()
)

print(config)
# AppConfig(debug=True, port=8080, database=DatabaseConfig(...))
# secret_keyλŠ” 좜λ ₯ μ•ˆ 됨

이벀트 λ‘œκΉ…

from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, Any
from enum import Enum

class LogLevel(Enum):
    DEBUG = "DEBUG"
    INFO = "INFO"
    WARNING = "WARNING"
    ERROR = "ERROR"

@dataclass
class LogEntry:
    message: str
    level: LogLevel = LogLevel.INFO
    timestamp: datetime = field(default_factory=datetime.now)
    context: Dict[str, Any] = field(default_factory=dict)

    def __str__(self):
        ctx = ", ".join(f"{k}={v}" for k, v in self.context.items())
        return f"[{self.timestamp:%Y-%m-%d %H:%M:%S}] {self.level.value}: {self.message} | {ctx}"

# μ‚¬μš©
log = LogEntry(
    message="User login successful",
    level=LogLevel.INFO,
    context={"user_id": 123, "ip": "192.168.1.1"}
)
print(log)
# [2025-01-13 14:30:45] INFO: User login successful | user_id=123, ip=192.168.1.1

DTO (Data Transfer Object)

from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional

@dataclass
class CreateUserRequest:
    """μ‚¬μš©μž 생성 μš”μ²­ DTO"""
    username: str
    email: str
    password: str = field(repr=False)
    age: Optional[int] = None

    def __post_init__(self):
        # μœ νš¨μ„± 검증
        if not self.username or len(self.username) < 3:
            raise ValueError("μ‚¬μš©μžλͺ…은 3자 이상이어야 ν•©λ‹ˆλ‹€")
        if "@" not in self.email:
            raise ValueError("μ˜¬λ°”λ₯Έ 이메일 ν˜•μ‹μ΄ μ•„λ‹™λ‹ˆλ‹€")
        if len(self.password) < 8:
            raise ValueError("λΉ„λ°€λ²ˆν˜ΈλŠ” 8자 이상이어야 ν•©λ‹ˆλ‹€")
        if self.age is not None and self.age < 0:
            raise ValueError("λ‚˜μ΄λŠ” 0 이상이어야 ν•©λ‹ˆλ‹€")

@dataclass
class UserResponse:
    """μ‚¬μš©μž 응닡 DTO"""
    id: int
    username: str
    email: str
    age: Optional[int] = None
    created_at: datetime = field(default_factory=datetime.now)

    # passwordλŠ” 응닡에 ν¬ν•¨ν•˜μ§€ μ•ŠμŒ

# μ‚¬μš©
request = CreateUserRequest(
    username="hongildong",
    email="hong@example.com",
    password="secret123456",
    age=30
)

# μ„œλΉ„μŠ€ κ³„μΈ΅μ—μ„œ 처리 ν›„
response = UserResponse(
    id=1,
    username=request.username,
    email=request.email,
    age=request.age
)

print(response)
# UserResponse(id=1, username='hongildong', email='hong@example.com', age=30, created_at=...)

10. dataclass vs NamedTuple vs 일반 클래슀

λΉ„κ΅ν‘œ

νŠΉμ§• 일반 클래슀 dataclass NamedTuple
간결함 ❌ λ³΄μΌλŸ¬ν”Œλ ˆμ΄νŠΈ 많음 βœ… κ°„κ²° βœ… 맀우 κ°„κ²°
κ°€λ³€μ„± βœ… κ°€λ³€ βœ… κ°€λ³€ (frozen=False) ❌ λΆˆλ³€
λ©”μ„œλ“œ μΆ”κ°€ βœ… 자유둭게 βœ… 자유둭게 βœ… κ°€λŠ₯
상속 βœ… 자유둭게 βœ… κ°€λŠ₯ ⚠️ μ œν•œμ 
κΈ°λ³Έκ°’ βœ… μ„€μ • κ°€λŠ₯ βœ… μ„€μ • κ°€λŠ₯ βœ… μ„€μ • κ°€λŠ₯
νƒ€μž… 힌트 ⚠️ μˆ˜λ™ βœ… ν•„μˆ˜ βœ… ν•„μˆ˜
dict ν‚€ μ‚¬μš© βœ… κΈ°λ³Έ κ°€λŠ₯ (단 __eq__만 μž¬μ •μ˜ν•˜λ©΄ __hash__=None λ˜μ–΄ λΆˆκ°€) ⚠️ frozen=True ν•„μš” (κΈ°λ³Έ dataclassλŠ” __eq__ μžλ™ μƒμ„±μœΌλ‘œ hash λΉ„ν™œμ„±ν™”) ⚠️ ν•„λ“œ 값이 λͺ¨λ‘ hashable일 λ•Œλ§Œ κ°€λŠ₯ (list/dict 같은 κ°€λ³€ 값을 λ‹΄μœΌλ©΄ unhashable)

μ½”λ“œ 비ꡐ

from dataclasses import dataclass
from typing import NamedTuple

# 1. 일반 클래슀 (κ°€μž₯ μœ μ—°)
class PersonClass:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def __repr__(self):
        return f"PersonClass(name={self.name!r}, age={self.age!r})"

    def __eq__(self, other):
        if not isinstance(other, PersonClass):
            return NotImplemented
        return (self.name, self.age) == (other.name, other.age)

    def greet(self):
        return f"μ•ˆλ…•ν•˜μ„Έμš”, {self.name}μž…λ‹ˆλ‹€"

# 2. dataclass (쀑간)
@dataclass
class PersonDataclass:
    name: str
    age: int

    def greet(self):
        return f"μ•ˆλ…•ν•˜μ„Έμš”, {self.name}μž…λ‹ˆλ‹€"

# 3. NamedTuple (λΆˆλ³€)
class PersonTuple(NamedTuple):
    name: str
    age: int

    def greet(self):
        return f"μ•ˆλ…•ν•˜μ„Έμš”, {self.name}μž…λ‹ˆλ‹€"

μ–Έμ œ 무엇을 μ“ΈκΉŒ?

# βœ… λΆˆλ³€ 데이터 + κ°„λ‹¨ν•œ ꡬ쑰 β†’ NamedTuple
class Point(NamedTuple):
    x: int
    y: int

# βœ… 데이터 쀑심 + 간결함 ν•„μš” β†’ dataclass
@dataclass
class User:
    username: str
    email: str
    age: int

# βœ… λ³΅μž‘ν•œ 둜직 + λ§Žμ€ λ©”μ„œλ“œ β†’ 일반 클래슀
class UserService:
    def __init__(self, db_connection):
        self.db = db_connection

    def authenticate(self, username, password):
        # λ³΅μž‘ν•œ 인증 둜직
        pass

    def get_user(self, user_id):
        # DB 쑰회 둜직
        pass

11. μ£Όμ˜μ‚¬ν•­

1. νƒ€μž… 힌트 ν•„μˆ˜

# ❌ νƒ€μž… 힌트 μ—†μœΌλ©΄ dataclass ν•„λ“œλ‘œ 인식 μ•ˆ 됨
@dataclass
class Wrong:
    name = "default"  # 클래슀 λ³€μˆ˜λ‘œ 인식됨 (dataclass ν•„λ“œ μ•„λ‹˜!)

# βœ… νƒ€μž… 힌트 ν•„μˆ˜
@dataclass
class Correct:
    name: str = "default"  # dataclass ν•„λ“œ

2. κ°€λ³€ κΈ°λ³Έκ°’ κΈˆμ§€

# ❌ μ ˆλŒ€ μ•ˆ 됨
@dataclass
class Wrong:
    items: list = []  # ValueError!

# βœ… field(default_factory) μ‚¬μš©
@dataclass
class Correct:
    items: list = field(default_factory=list)

3. slots μ‚¬μš© (Python 3.10+)

# λ©”λͺ¨λ¦¬ μ΅œμ ν™”
@dataclass(slots=True)
class Point:
    x: int
    y: int

# μž₯점:
# - 더 적은 λ©”λͺ¨λ¦¬ μ‚¬μš©
# - 속성 μ ‘κ·Ό 속도 ν–₯상
# 단점:
# - __dict__κ°€ μ—†μ–΄μ„œ 동적 속성 μΆ”κ°€ λΆˆκ°€

4. kw_only (Python 3.10+)

# ν‚€μ›Œλ“œ μ „μš© 인수둜 κ°•μ œ
@dataclass(kw_only=True)
class Config:
    host: str
    port: int

# ❌ μœ„μΉ˜ 인수 λΆˆκ°€
# config = Config("localhost", 8080)  # TypeError!

# βœ… ν‚€μ›Œλ“œ 인수만 κ°€λŠ₯
config = Config(host="localhost", port=8080)

12. κ³ κΈ‰ ν™œμš©

μ€‘μ²©λœ dataclass

@dataclass
class Address:
    street: str
    city: str
    zipcode: str

@dataclass
class Person:
    name: str
    age: int
    address: Address

person = Person(
    name="홍길동",
    age=30,
    address=Address("κ°•λ‚¨λŒ€λ‘œ 123", "μ„œμšΈ", "06000")
)

print(person)
# Person(name='홍길동', age=30, address=Address(street='κ°•λ‚¨λŒ€λ‘œ 123', city='μ„œμšΈ', zipcode='06000'))

# asdict둜 쀑첩 λ³€ν™˜
from dataclasses import asdict
print(asdict(person))
# {'name': '홍길동', 'age': 30, 'address': {'street': 'κ°•λ‚¨λŒ€λ‘œ 123', 'city': 'μ„œμšΈ', 'zipcode': '06000'}}

νŒ©ν† λ¦¬ λ©”μ„œλ“œ νŒ¨ν„΄

from dataclasses import dataclass
from typing import Dict, Any

@dataclass
class User:
    id: int
    username: str
    email: str

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'User':
        """λ”•μ…”λ„ˆλ¦¬μ—μ„œ User 생성"""
        return cls(
            id=data['id'],
            username=data['username'],
            email=data['email']
        )

    @classmethod
    def from_json(cls, json_str: str) -> 'User':
        """JSON λ¬Έμžμ—΄μ—μ„œ User 생성"""
        import json
        data = json.loads(json_str)
        return cls.from_dict(data)

# μ‚¬μš©
user_data = {"id": 1, "username": "hong", "email": "hong@example.com"}
user = User.from_dict(user_data)

json_str = '{"id": 2, "username": "kim", "email": "kim@example.com"}'
user2 = User.from_json(json_str)

μš”μ•½

dataclassλž€?

데이터λ₯Ό μ €μž₯ν•˜λŠ” 클래슀λ₯Ό κ°„νŽΈν•˜κ²Œ λ§Œλ“€μ–΄μ£ΌλŠ” λ°μ½”λ ˆμ΄ν„°λ‘œ, λ³΄μΌλŸ¬ν”Œλ ˆμ΄νŠΈ μ½”λ“œλ₯Ό μžλ™ μƒμ„±ν•©λ‹ˆλ‹€.

핡심 κΈ°λŠ₯

from dataclasses import dataclass, field

@dataclass(frozen=True, order=True)
class Person:
    name: str
    age: int = 0
    tags: list = field(default_factory=list, repr=False)

    def __post_init__(self):
        if self.age < 0:
            raise ValueError("λ‚˜μ΄λŠ” 0 이상이어야 ν•©λ‹ˆλ‹€")

μžλ™ μƒμ„±λ˜λŠ” 것듀

  • __init__: μƒμ„±μž
  • __repr__: λ¬Έμžμ—΄ ν‘œν˜„
  • __eq__: 동등 비ꡐ
  • __hash__: ν•΄μ‹œ (frozen=True 일 λ•Œ)
  • __lt__, __le__, __gt__, __ge__: μˆœμ„œ 비ꡐ (order=True 일 λ•Œ)

μ£Όμš” μ˜΅μ…˜

μ˜΅μ…˜ μ„€λͺ… κΈ°λ³Έκ°’
init __init__ 생성 True
repr __repr__ 생성 True
eq __eq__ 생성 True
order 비ꡐ λ©”μ„œλ“œ 생성 False
frozen λΆˆλ³€ 객체 False
slots __slots__ μ‚¬μš© (3.10+) False
kw_only ν‚€μ›Œλ“œ μ „μš© 인수 (3.10+) False

μ–Έμ œ μ‚¬μš©?

βœ… μ‚¬μš©ν•˜κΈ° 쒋은 경우:

  • 데이터λ₯Ό μ €μž₯ν•˜λŠ” κ°„λ‹¨ν•œ 클래슀
  • μ„€μ • 객체, λͺ¨λΈ, DTO (Data Transfer Object)
  • API 응닡/μš”μ²­ λͺ¨λΈ
  • λ³΄μΌλŸ¬ν”Œλ ˆμ΄νŠΈ μ½”λ“œ 쀄이기

❌ μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” 것이 쒋은 경우:

  • λ³΅μž‘ν•œ λΉ„μ¦ˆλ‹ˆμŠ€ 둜직이 λ§Žμ€ 클래슀
  • λ§Žμ€ λ©”μ„œλ“œμ™€ μƒνƒœ 관리가 ν•„μš”ν•œ 경우
  • 상속 ꡬ쑰가 λ³΅μž‘ν•œ 경우

λΉ λ₯Έ μ°Έμ‘°

from dataclasses import dataclass, field

@dataclass
class Example:
    # ν•„μˆ˜ ν•„λ“œ (κΈ°λ³Έκ°’ μ—†μŒ β€” λ°˜λ“œμ‹œ λ¨Όμ €)
    required_field: str

    # κΈ°λ³Έκ°’
    optional_field: int = 0

    # κ°€λ³€ κΈ°λ³Έκ°’
    items: list = field(default_factory=list)

    # __repr__μ—μ„œ μˆ¨κΉ€ β€” κΈ°λ³Έκ°’ μžˆλŠ” ν•„λ“œ 사이에 두렀면 default도 ν•¨κ»˜
    secret: str = field(default="", repr=False)

    # μ΄ˆκΈ°ν™”μ—μ„œ μ œμ™Έ
    computed: float = field(init=False)

    # λΉ„κ΅μ—μ„œ μ œμ™Έ
    metadata: dict = field(default_factory=dict, compare=False)

    def __post_init__(self):
        # μ΄ˆκΈ°ν™” ν›„ 처리
        self.computed = self.required_field * self.optional_field

참고 자료