Alert
์ด ๊ธ์ Claude Code์ ๋์์ ๋ฐ์ ์์ฑ๋์์ต๋๋ค
TL;DR
@dataclass๋ ๋ฐ์ดํฐ๋ฅผ ๋ด๋ ํด๋์ค์ boilerplate(__init__,__repr__,__eq__๋ฑ)๋ฅผ ์๋ ์์ฑํด์ฃผ๋ ๋ฐ์ฝ๋ ์ดํฐ- Python 3.7์์ ๋์ (PEP 557), โ๊ธฐ๋ณธ๊ฐ์ด ์๋ mutable namedtupleโ๋ก ์ดํดํ๋ฉด ์ฝ๋ค
- ์ค์ ๊ฐ์ฒด, API ์๋ต ๋งคํ, DTO ๋ฑ ๊ฐ์ ๊ตฌ์กฐํํด์ ๋ด๋ ๋ชจ๋ ๊ณณ์์ ์ ์ฉํ๋ค
Sources
1. ์ dataclass์ธ๊ฐ
Python์์ ๋ฐ์ดํฐ๋ฅผ ๋ด๋ ํด๋์ค๋ฅผ ๋ง๋ค๋ ค๋ฉด ๋ฐ๋ณต ์ฝ๋๊ฐ ๋ง๋ค.
์ผ๋ฐ ํด๋์ค๋ก ์์ฑํ๋ฉด
class User:
def __init__(self, name: str, age: int, email: str):
self.name = name
self.age = age
self.email = email
def __repr__(self):
return f"User(name={self.name!r}, age={self.age!r}, email={self.email!r})"
def __eq__(self, other):
if not isinstance(other, User):
return NotImplemented
return (self.name, self.age, self.email) == (other.name, other.age, other.email)ํ๋๊ฐ 3๊ฐ๋ฟ์ธ๋ฐ 20์ค์ด๋ค. ํ๋๊ฐ ๋์ด๋ ๋๋ง๋ค __init__, __repr__, __eq__๋ฅผ ์ ๋ถ ์์ ํด์ผ ํ๋ค.
dataclass๋ก ์์ฑํ๋ฉด
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
email: str์ด๊ฒ ์ ๋ถ๋ค. __init__, __repr__, __eq__๊ฐ ์๋ ์์ฑ๋๋ค.
u1 = User("alice", 30, "alice@example.com")
u2 = User("alice", 30, "alice@example.com")
print(u1) # User(name='alice', age=30, email='alice@example.com')
print(u1 == u2) # Trueํต์ฌ
dataclass๋ ์๋ก์ด ์๋ฃ๊ตฌ์กฐ๊ฐ ์๋๋ค. ์ผ๋ฐ ํด๋์ค์ boilerplate ๋ฉ์๋๋ฅผ ์๋์ผ๋ก ๋ถ์ฌ์ฃผ๋ ๋ฐ์ฝ๋ ์ดํฐ์ผ ๋ฟ์ด๋ค. ์์ฑ๋ ํด๋์ค๋ ์ผ๋ฐ ํด๋์ค์ ์์ ํ ๋์ผํ๊ฒ ๋์ํ๋ค.
2. ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
์๋ ์์ฑ๋๋ ๋ฉ์๋ (boilerplate)
Python ํด๋์ค์๋ __๋ก ๊ฐ์ธ์ง ํน์ ๋ฉ์๋(dunder method)๊ฐ ์๋ค. ๊ฐ์ฒด๋ฅผ ์ถ๋ ฅํ๊ฑฐ๋, ๋น๊ตํ๊ฑฐ๋, ํด์๊ฐ์ ๊ตฌํ ๋ Python์ด ๋ด๋ถ์ ์ผ๋ก ํธ์ถํ๋ ๋ฉ์๋๋ค. ์ด๊ฒ๋ค์ ์ง์ ์ ์ํ์ง ์์ผ๋ฉด object์์ ์์๋ฐ์ ๊ธฐ๋ณธ ๋์์ด ์ ์ฉ๋๋๋ฐ, ๋๋ถ๋ถ ๋ฐ์ดํฐ ํด๋์ค์์ ์ํ๋ ๋์๊ณผ ๋ค๋ฅด๋ค.
| ๋ฉ์๋ | ์ญํ | ์ ์ ์ ํ์ ๋ (์ผ๋ฐ ํด๋์ค) | dataclass ์๋ ์์ฑ |
|---|---|---|---|
__init__() | ๊ฐ์ฒด ์์ฑ ์ ์ด๊ธฐํ | ์ธ์ ์๋ ๋น ์์ฑ์ | ํ๋๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์ self.ํ๋ = ๊ฐ |
__repr__() | print(), ๋๋ฒ๊น
์ ์ถ๋ ฅ | <User object at 0x7f...> (๋ฉ๋ชจ๋ฆฌ ์ฃผ์) | User(name='alice', age=30) |
__eq__() | == ๋น๊ต | ๊ฐ์ฒด identity ๋น๊ต (is์ ๋์ผ) | ํ๋ ๊ฐ ๋น๊ต |
__hash__() | dict ํค, set ์์๋ก ์ฌ์ฉํ ๋ | id() ๊ธฐ๋ฐ ํด์ | frozen=True์ผ ๋ ํ๋ ๊ธฐ๋ฐ ํด์ |
__lt__() ๋ฑ | <, <=, >, >= ๋น๊ต | TypeError (๋น๊ต ๋ถ๊ฐ) | order=True์ผ ๋ ํ๋ ํํ ๋น๊ต |
๊ฐ์ฅ ์์ฃผ ๊ฒช๋ ํจ์ ์ __eq__๋ค.
# ์ผ๋ฐ ํด๋์ค โ ๊ฐ์ด ๊ฐ์๋ ๋ค๋ฅธ ๊ฐ์ฒด๋ฉด False
class User:
def __init__(self, name):
self.name = name
print(User("alice") == User("alice")) # False (๋ฉ๋ชจ๋ฆฌ ์ฃผ์๊ฐ ๋ค๋ฅด๋๊น)# dataclass โ ๊ฐ์ด ๊ฐ์ผ๋ฉด True
@dataclass
class User:
name: str
print(User("alice") == User("alice")) # True (ํ๋ ๊ฐ์ ๋น๊ตํ๋๊น)__repr__๋ ๋ง์ฐฌ๊ฐ์ง๋ค.
# ์ผ๋ฐ ํด๋์ค
class User:
def __init__(self, name):
self.name = name
print(User("alice")) # <User object at 0x7f3b2c1d4a90> โ ๋๋ฒ๊น
ํ ๋ ์ธ๋ชจ์๋ค# dataclass
@dataclass
class User:
name: str
print(User("alice")) # User(name='alice') โ ๋ฐ๋ก ๋ด์ฉ์ด ๋ณด์ธ๋ค๊ธฐ๋ณธ๊ฐ ์ง์
@dataclass
class Config:
host: str = "localhost"
port: int = 8080
debug: bool = Falsec = Config()
print(c) # Config(host='localhost', port=8080, debug=False)
c2 = Config(host="0.0.0.0", debug=True)
print(c2) # Config(host='0.0.0.0', port=8080, debug=True)์ผ๋ฐ ํจ์์ ๊ธฐ๋ณธ ์ธ์์ ๊ฐ์ ๊ท์น์ด ์ ์ฉ๋๋ค. ๊ธฐ๋ณธ๊ฐ์ด ์๋ ํ๋๊ฐ ์๋ ํ๋๋ณด๋ค ์์ ์์ผ ํ๋ค.
# โ TypeError ๋ฐ์
@dataclass
class Bad:
x: int = 0
y: int # ๊ธฐ๋ณธ๊ฐ ์๋ ํ๋๊ฐ ๋ค์ ์ค๋ฉด ์ ๋จ
# โ
์ฌ๋ฐ๋ฅธ ์์
@dataclass
class Good:
y: int
x: int = 0๋ฉ์๋ ์ถ๊ฐ
dataclass๋ผ๊ณ ํด์ ๋ฉ์๋๋ฅผ ๋ชป ๋ฃ๋ ๊ฒ ์๋๋ค. ์ผ๋ฐ ํด๋์ค์ ๋์ผํ๊ฒ ๋ฉ์๋๋ฅผ ์ถ๊ฐํ ์ ์๋ค.
@dataclass
class Rectangle:
width: float
height: float
def area(self) -> float:
return self.width * self.height
def is_square(self) -> bool:
return self.width == self.heightr = Rectangle(3.0, 4.0)
print(r.area()) # 12.0
print(r.is_square()) # False3. ๊ธฐ์กด ๋ฐฉ์๊ณผ ๋น๊ต
๋ฐ์ดํฐ๋ฅผ ๋ด๋ ๋ฐฉ๋ฒ์ ์ฌ๋ฌ ๊ฐ์ง๋ค. ๊ฐ๊ฐ ์ธ์ ์ ํฉํ์ง ๋น๊ตํ๋ค.
dict
user = {"name": "alice", "age": 30, "email": "alice@example.com"}NamedTuple
from typing import NamedTuple
class User(NamedTuple):
name: str
age: int
email: strdataclass
from dataclasses import dataclass
@dataclass
class User:
name: str
age: int
email: str๋น๊ตํ
| ๊ธฐ์ค | dict | NamedTuple | dataclass |
|---|---|---|---|
| ํ์ ํํ | X (๊ฐ์ ๋ํด ์์) | O | O |
์๋ __repr__ | X | O | O |
์๋ __eq__ | O (๊ฐ ๋น๊ต) | O | O |
| mutable | O | X (๋ถ๋ณ) | O (๊ธฐ๋ณธ) / X (frozen=True) |
| ๊ธฐ๋ณธ๊ฐ | O | O | O |
| ์์ฑ ์ ๊ทผ | d["name"] | u.name | u.name |
| IDE ์๋์์ฑ | X | O | O |
| ๋ฉ์๋ ์ถ๊ฐ | X | ์ ํ์ | O |
| ์์ | X | ์ ํ์ | O |
| ์ธํฉ | X | O (*u) | X (๋ณ๋ ๊ตฌํ ํ์) |
์ธ์ ๋ญ ์ธ๊น
- dict โ ์คํค๋ง๊ฐ ์ ๋์ ์ด๊ฑฐ๋ JSON์ ์์๋ก ๋ค๋ฃฐ ๋
- NamedTuple โ ๋ถ๋ณ์ด์ด์ผ ํ๊ณ , ํํ ์ธํฉ์ด ํ์ํ๋ฉฐ, ๊ฐ๋ณ๊ฒ ์ธ ๋
- dataclass โ ํ๋๊ฐ ๋ช ํํ๊ณ , mutable์ด ํ์ํ๊ฑฐ๋, ๋ฉ์๋/์์์ด ํ์ํ ๋
dataclass์ ์ธํฉ(unpack)
NamedTuple์ ๋ด๋ถ๊ฐ tuple์ด๋ผ x, y = point๊ฐ ๋ฐ๋ก ๋์ง๋ง, dataclass๋ ์ผ๋ฐ ํด๋์ค๋ผ์ __iter__๊ฐ ์์ด ๊ธฐ๋ณธ์ ์ผ๋ก ์ธํฉ์ด ์ ๋๋ค.
from typing import NamedTuple
from dataclasses import dataclass, astuple, asdict
class NTPoint(NamedTuple):
x: int
y: int
@dataclass
class DCPoint:
x: int
y: int
x, y = NTPoint(1, 2) # OK โ tuple์ด๋๊น
x, y = DCPoint(1, 2) # TypeError: cannot unpack non-iterable DCPoint object์ธํฉ์ด ํ์ํ๋ฉด astuple()์ ์ฐ๊ฑฐ๋, __iter__๋ฅผ ์ง์ ์ถ๊ฐํ๋ฉด ๋๋ค.
# ๋ฐฉ๋ฒ 1: astuple()๋ก ๋ณํ ํ ์ธํฉ
from dataclasses import astuple
p = DCPoint(1, 2)
x, y = astuple(p)
# ๋ฐฉ๋ฒ 2: __iter__ ๊ตฌํ
@dataclass
class Point:
x: int
y: int
def __iter__(self):
return iter(astuple(self))
x, y = Point(1, 2) # OK๋๋ถ๋ถ์ ๊ฒฝ์ฐ .x, .y๋ก ์ ๊ทผํ๋ ๊ฒ ๋ช
์์ ์ด๊ณ ๊ถ์ฅ๋๋ ๋ฐฉ์์ด๋ค. ์ธํฉ์ ์ขํ์ฒ๋ผ ์์๊ฐ ์๋ช
ํ ๊ฒฝ์ฐ์๋ง ์ฐ๋ ๊ฒ ์ข๋ค.
NamedTuple์ ํจ์
NamedTuple์ ๋ด๋ถ์ ์ผ๋ก tuple์ด๋ผ์ ๋ค๋ฅธ ํ์ ๋ผ๋ฆฌ๋ ๊ฐ๋ง ๊ฐ์ผ๋ฉด ๋์ผํ๋ค๊ณ ํ๋จํ๋ค.
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
class Pair(NamedTuple):
first: int
second: int
print(Point(1, 2) == Pair(1, 2)) # True โ ์๋ํ์ง ์์ ๋์dataclass๋ ํด๋์ค ํ์ ๊น์ง ๋น๊ตํ๋ฏ๋ก ์ด ๋ฌธ์ ๊ฐ ์๋ค.
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
@dataclass
class Pair:
first: int
second: int
print(Point(1, 2) == Pair(1, 2)) # False4. field()์ ๊ธฐ๋ณธ๊ฐ
field() ๊ธฐ๋ณธ ์ฌ์ฉ
field()๋ฅผ ํตํด ํ๋๋ณ๋ก ์ธ๋ฐํ ์ ์ด๊ฐ ๊ฐ๋ฅํ๋ค.
from dataclasses import dataclass, field
@dataclass
class Product:
name: str
price: float
tags: list[str] = field(default_factory=list)
internal_id: str = field(repr=False, default="N/A")
discount: float = field(compare=False, default=0.0)p = Product("๋
ธํธ๋ถ", 1500000.0, ["์ ์๊ธฐ๊ธฐ", "์ปดํจํฐ"])
print(p) # Product(name='๋
ธํธ๋ถ', price=1500000.0, tags=['์ ์๊ธฐ๊ธฐ', '์ปดํจํฐ'], discount=0.0)
# internal_id๋ repr=False๋ผ ์ถ๋ ฅ์์ ์ ์ธ๋๋คfield() ์ฃผ์ ์ต์
| ์ต์ | ๊ธฐ๋ณธ๊ฐ | ์ค๋ช |
|---|---|---|
default | MISSING | ํ๋์ ๊ธฐ๋ณธ๊ฐ |
default_factory | MISSING | ๊ธฐ๋ณธ๊ฐ์ ์์ฑํ๋ ํจ์ (mutable ๊ธฐ๋ณธ๊ฐ์ ํ์) |
init | True | __init__ ํ๋ผ๋ฏธํฐ์ ํฌํจํ ์ง |
repr | True | __repr__ ์ถ๋ ฅ์ ํฌํจํ ์ง |
compare | True | __eq__ ๋ฑ ๋น๊ต์ ํฌํจํ ์ง |
hash | None | __hash__์ ํฌํจํ ์ง (None์ด๋ฉด compare ๊ฐ์ ๋ฐ๋ฆ) |
kw_only | False | keyword-only ํ๋ผ๋ฏธํฐ๋ก ์ค์ |
metadata | None | ์ปค์คํ ๋ฉํ๋ฐ์ดํฐ ๋์ ๋๋ฆฌ |
๊ฐ๋ณ ๊ธฐ๋ณธ๊ฐ ํจ์
Python์์ mutable ๊ธฐ๋ณธ๊ฐ์ ๋ชจ๋ ์ธ์คํด์ค๊ฐ ๊ฐ์ ๊ฐ์ฒด๋ฅผ ๊ณต์ ํ๋ ์ ๋ช ํ ํจ์ ์ด๋ค. dataclass๋ ์ด๊ฑธ ์ปดํ์ผ ํ์์ ์ก์์ค๋ค.
# โ dataclass๊ฐ TypeError๋ฅผ ๋ฐ์์ํจ๋ค
@dataclass
class BadExample:
items: list = []
# TypeError: mutable default <class 'list'> for field items is not allowed:
# use default_factory# โ
default_factory ์ฌ์ฉ
@dataclass
class GoodExample:
items: list = field(default_factory=list)
config: dict = field(default_factory=dict)
scores: set = field(default_factory=set)a = GoodExample()
b = GoodExample()
a.items.append(1)
print(a.items) # [1]
print(b.items) # [] โ ๊ฐ ์ธ์คํด์ค๊ฐ ๋
๋ฆฝ์ ์ธ ๋ฆฌ์คํธ๋ฅผ ๊ฐ์ง๋ค๋ณต์กํ ๊ธฐ๋ณธ๊ฐ
default_factory์๋ ์ด๋ค callable์ด๋ ๋ฃ์ ์ ์๋ค.
from dataclasses import dataclass, field
from datetime import datetime
@dataclass
class LogEntry:
message: str
level: str = "INFO"
timestamp: datetime = field(default_factory=datetime.now)
metadata: dict = field(default_factory=lambda: {"version": "1.0"})log = LogEntry("์๋ฒ ์์")
print(log)
# LogEntry(message='์๋ฒ ์์', level='INFO', timestamp=datetime(...), metadata={'version': '1.0'})init=False์ ๊ณ์ฐ ํ๋
init=False๋ก ์ง์ ํ๋ฉด ์์ฑ์์์ ๋ฐ์ง ์๊ณ , __post_init__์์ ๊ณ์ฐํ ์ ์๋ค.
@dataclass
class Circle:
radius: float
area: float = field(init=False)
def __post_init__(self):
self.area = 3.14159 * self.radius ** 2c = Circle(5.0)
print(c) # Circle(radius=5.0, area=78.53975)metadata ํ์ฉ
metadata๋ ์ง๋ ฌํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๊ฒ์ฆ ๋ก์ง์์ ํ๋ ์ ๋ณด๋ฅผ ์ฐธ์กฐํ ๋ ์ ์ฉํ๋ค.
from dataclasses import dataclass, field, fields
@dataclass
class APIResponse:
status_code: int = field(metadata={"json_key": "statusCode"})
body: str = field(metadata={"json_key": "responseBody"})
headers: dict = field(default_factory=dict, metadata={"json_key": "headers"})
# ๋ฉํ๋ฐ์ดํฐ ์กฐํ
for f in fields(APIResponse):
print(f"{f.name} -> JSON key: {f.metadata.get('json_key')}")
# status_code -> JSON key: statusCode
# body -> JSON key: responseBody
# headers -> JSON key: headers5. ๋ฐ์ฝ๋ ์ดํฐ ์ต์
@dataclass ๋ฐ์ฝ๋ ์ดํฐ์๋ ํด๋์ค ์ ์ฒด์ ๋์์ ์ ์ดํ๋ ํ๋ผ๋ฏธํฐ๊ฐ ์๋ค.
@dataclass(init=True, repr=True, eq=True, order=False,
frozen=False, slots=False, kw_only=False)frozen โ ๋ถ๋ณ ๊ฐ์ฒด ๋ง๋ค๊ธฐ
frozen=True๋ก ์ค์ ํ๋ฉด ์ธ์คํด์ค ์์ฑ ํ ํ๋ ๊ฐ์ ๋ณ๊ฒฝํ ์ ์๋ค.
@dataclass(frozen=True)
class Coordinate:
lat: float
lng: floatc = Coordinate(37.5665, 126.9780)
c.lat = 0.0 # FrozenInstanceError ๋ฐ์frozen dataclass๋ __hash__๊ฐ ์๋ ์์ฑ๋๋ฏ๋ก dict ํค๋ set ์์๋ก ์ฌ์ฉํ ์ ์๋ค.
locations = {
Coordinate(37.5665, 126.9780): "์์ธ",
Coordinate(35.1796, 129.0756): "๋ถ์ฐ",
}
print(locations[Coordinate(37.5665, 126.9780)]) # ์์ธorder โ ๋น๊ต ์ฐ์ฐ์ ์๋ ์์ฑ
order=True๋ก ์ค์ ํ๋ฉด <, <=, >, >= ์ฐ์ฐ์๊ฐ ์๋ ์์ฑ๋๋ค. ํ๋๋ฅผ ํํ๋ก ๋ณํํด์ ์์๋๋ก ๋น๊ตํ๋ค.
@dataclass(order=True)
class Version:
major: int
minor: int
patch: intversions = [Version(2, 1, 0), Version(1, 9, 5), Version(2, 0, 3)]
print(sorted(versions))
# [Version(major=1, minor=9, patch=5), Version(major=2, minor=0, patch=3), Version(major=2, minor=1, patch=0)]
print(Version(2, 0, 0) > Version(1, 9, 9)) # Trueslots โ ๋ฉ๋ชจ๋ฆฌ ์ต์ ํ
slots=True(Python 3.10+)๋ก ์ค์ ํ๋ฉด __slots__๊ฐ ์๋ ์์ฑ๋๋ค. ์ด๊ฒ ์ ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ ์ฝํ๋์ง ์ดํดํ๋ ค๋ฉด Python์ด ๊ฐ์ฒด์ ์์ฑ์ ์ ์ฅํ๋ ๋ฐฉ์์ ์์์ผ ํ๋ค.
๊ธฐ๋ณธ ๋์ โ __dict__ ๋ฐฉ์
์ผ๋ฐ Python ๊ฐ์ฒด๋ ๋ด๋ถ์ __dict__๋ผ๋ ๋์
๋๋ฆฌ๋ฅผ ๊ฐ๊ณ ์๋ค. ์์ฑ์ ์ถ๊ฐํ๋ฉด ์ด ๋์
๋๋ฆฌ์ key-value๋ก ์ ์ฅ๋๋ค.
@dataclass
class Point:
x: float
y: float
p = Point(1.0, 2.0)
print(p.__dict__) # {'x': 1.0, 'y': 2.0}
# ๋์
๋๋ฆฌ๋ผ์ ์๋ฌด ์์ฑ์ด๋ ์์ ๋กญ๊ฒ ์ถ๊ฐํ ์ ์๋ค
p.z = 3.0
print(p.__dict__) # {'x': 1.0, 'y': 2.0, 'z': 3.0}๋์ ๋๋ฆฌ๋ ์ ์ฐํ์ง๋ง ์ค๋ฒํค๋๊ฐ ํฌ๋ค. ํด์ ํ ์ด๋ธ ์์ฒด์ ๋ฉ๋ชจ๋ฆฌ, ํค ๋ฌธ์์ด ๊ฐ์ฒด, ๋์ ๋ฆฌ์ฌ์ด์ง ๋ฑ์ด ์ธ์คํด์ค๋ง๋ค ๋ถ๋๋ค.
__slots__ ๋ฐฉ์
__slots__๋ฅผ ์ฌ์ฉํ๋ฉด __dict__๋ฅผ ์์ฑํ์ง ์๋๋ค. ๋์ ์์ฑ์ **๊ณ ์ ๋ ์์น์ ์ฌ๋กฏ(๋ฐฐ์ด)**์ ์ ์ฅํ๋ค. C ๊ตฌ์กฐ์ฒด์ ํ๋์ฒ๋ผ ๊ฐ ์์ฑ์ด ๋ฏธ๋ฆฌ ์ ํด์ง ์คํ์
์ ์์นํ๋ ๋ฐฉ์์ด๋ค.
@dataclass(slots=True)
class Point:
x: float
y: float
p = Point(1.0, 2.0)
print(p.__slots__) # ('x', 'y')
# __dict__๊ฐ ์๋ค
hasattr(p, '__dict__') # False
# ์ ์ธํ์ง ์์ ์์ฑ์ ์ถ๊ฐํ ์ ์๋ค
p.z = 3.0 # AttributeError: 'Point' object has no attribute 'z'์ค์ ๋ฉ๋ชจ๋ฆฌ ์ฐจ์ด
import sys
from dataclasses import dataclass
@dataclass
class DictPoint:
x: float
y: float
@dataclass(slots=True)
class SlotPoint:
x: float
y: float
d = DictPoint(1.0, 2.0)
s = SlotPoint(1.0, 2.0)
print(sys.getsizeof(d) + sys.getsizeof(d.__dict__)) # 344 bytes
print(sys.getsizeof(s)) # 48 bytes์ธ์คํด์ค ํ๋๋น ์์ญ๋ฐฑ ๋ฐ์ดํธ ์ฐจ์ด๊ฐ ๋๋ฏ๋ก, 100๋ง ๊ฐ๋ฅผ ๋ง๋ค๋ฉด ์์ญ์๋ฐฑ MB ์ฐจ์ด๊ฐ ๋๋ค.
์ผ๋ฐ (dict): [๊ฐ์ฒด ํค๋] โ [__dict__] โ {ํด์ ํ
์ด๋ธ: "x"โ1.0, "y"โ2.0}
slots: [๊ฐ์ฒด ํค๋] โ [slot 0: 1.0] [slot 1: 2.0]
slots๋ ์ธ์ ์ธ๊น
์ธ์คํด์ค๋ฅผ ์๋ง~์๋ฐฑ๋ง ๊ฐ ์์ฑํ๋ ๊ฒฝ์ฐ(๋ฐ์ดํฐ ํ์ดํ๋ผ์ธ, ์ด๋ฒคํธ ๋ก๊ทธ ๋ฑ)์ ๋ฉ๋ชจ๋ฆฌ ์ฐจ์ด๊ฐ ์ฒด๊ฐ๋๋ค. ์๋์ด๋ฉด ์ฒด๊ฐ ์๋ค.
slots์ ์ ์ฝ
- ์ ์ธํ์ง ์์ ์์ฑ์ ๋์ ์ผ๋ก ์ถ๊ฐํ ์ ์๋ค
- ๋ค์ค ์์ ์ ๋ถ๋ชจ ํด๋์ค๋
__slots__๋ฅผ ์ ์ํด์ผ ์ ๋๋ก ๋์ํ๋ค__dict__๊ฐ ์์ผ๋ฏ๋กvars(obj)๋์dataclasses.fields()๋ก ํ๋๋ฅผ ์กฐํํด์ผ ํ๋ค
kw_only โ keyword-only ํ๋ผ๋ฏธํฐ ๊ฐ์
kw_only=True(Python 3.10+)๋ก ์ค์ ํ๋ฉด ๋ชจ๋ ํ๋๊ฐ keyword-only๊ฐ ๋๋ค. ํ๋ ์๊ฐ ๋ง์ ๋ ์ค์ ๋ฐฉ์ง์ ์ข๋ค.
@dataclass(kw_only=True)
class DatabaseConfig:
host: str
port: int
user: str
password: str
database: str# โ TypeError โ ์์น ์ธ์๋ก ์ ๋ฌ ๋ถ๊ฐ
db = DatabaseConfig("localhost", 5432, "admin", "secret", "mydb")
# โ
๋ฐ๋์ keyword๋ก ์ ๋ฌ
db = DatabaseConfig(host="localhost", port=5432, user="admin",
password="secret", database="mydb")ํ๋ ๋จ์๋ก๋ ์ ์ดํ ์ ์๋ค. KW_ONLY ์ผํฐ๋ฌ์ ์ฌ์ฉํ๋ฉด ํด๋น ์์น ์ดํ์ ํ๋๋ง keyword-only๊ฐ ๋๋ค.
from dataclasses import dataclass, KW_ONLY
@dataclass
class Request:
url: str # ์์น ์ธ์ OK
method: str = "GET" # ์์น ์ธ์ OK
_: KW_ONLY
headers: dict = None # keyword-only
timeout: int = 30 # keyword-onlyr = Request("https://api.example.com", "POST", headers={"Authorization": "Bearer ..."})6. __post_init__๊ณผ InitVar
__post_init__ โ ์ด๊ธฐํ ํ์ฒ๋ฆฌ
__init__์ด ์คํ๋ ์งํ ์๋์ผ๋ก ํธ์ถ๋๋ค. ํ๋ ๊ฐ ๊ฒ์ฆ, ๊ณ์ฐ ํ๋ ์์ฑ ๋ฑ์ ์ฌ์ฉํ๋ค.
@dataclass
class Temperature:
celsius: float
fahrenheit: float = field(init=False)
kelvin: float = field(init=False)
def __post_init__(self):
self.fahrenheit = self.celsius * 9 / 5 + 32
self.kelvin = self.celsius + 273.15t = Temperature(100)
print(t)
# Temperature(celsius=100, fahrenheit=212.0, kelvin=373.15)๊ฐ ๊ฒ์ฆ
@dataclass
class Age:
value: int
def __post_init__(self):
if self.value < 0:
raise ValueError(f"๋์ด๋ ์์์ผ ์ ์์ต๋๋ค: {self.value}")
if self.value > 150:
raise ValueError(f"๋นํ์ค์ ์ธ ๋์ด์
๋๋ค: {self.value}")Age(25) # OK
Age(-1) # ValueError: ๋์ด๋ ์์์ผ ์ ์์ต๋๋ค: -1๊ฐ ์ ๊ทํ
@dataclass
class Tag:
name: str
def __post_init__(self):
self.name = self.name.strip().lower()t = Tag(" Python ")
print(t) # Tag(name='python')InitVar โ ์ด๊ธฐํ ์ ์ฉ ํ๋ผ๋ฏธํฐ
InitVar๋ก ์ ์ธํ ํ๋๋ __init__์๋ง ์กด์ฌํ๊ณ ์ธ์คํด์ค ์์ฑ์ผ๋ก๋ ์ ์ฅ๋์ง ์๋๋ค. __post_init__์ ์ธ์๋ก ์ ๋ฌ๋๋ค.
from dataclasses import dataclass, field, InitVar
@dataclass
class User:
name: str
age: int
birth_year: InitVar[int] = None
def __post_init__(self, birth_year: int | None):
if birth_year is not None:
self.age = 2026 - birth_yearu1 = User("alice", age=30)
print(u1) # User(name='alice', age=30)
u2 = User("bob", age=0, birth_year=1995)
print(u2) # User(name='bob', age=31)
# birth_year๋ ์ธ์คํด์ค์ ์ ์ฅ๋์ง ์๋๋ค์ค์ ์์ โ DB ์กฐํ ๊ฒฐ๊ณผ๋ก ์ด๊ธฐํ
from dataclasses import dataclass, field, InitVar
@dataclass
class Product:
name: str
price: float
category: str = field(init=False, default="๋ฏธ๋ถ๋ฅ")
raw_data: InitVar[dict | None] = None
def __post_init__(self, raw_data: dict | None):
if raw_data:
self.category = raw_data.get("category", "๋ฏธ๋ถ๋ฅ")
if "discount_rate" in raw_data:
self.price *= (1 - raw_data["discount_rate"])data = {"category": "์ ์๊ธฐ๊ธฐ", "discount_rate": 0.1}
p = Product("ํค๋ณด๋", 100000, raw_data=data)
print(p) # Product(name='ํค๋ณด๋', price=90000.0, category='์ ์๊ธฐ๊ธฐ')7. ์ ํธ ํจ์
dataclasses ๋ชจ๋์ ์ธ์คํด์ค๋ฅผ ๋ณํํ๊ฑฐ๋ ์กฐํํ๋ ์ ํธ ํจ์๋ฅผ ์ ๊ณตํ๋ค.
asdict() โ ๋์ ๋๋ฆฌ ๋ณํ
from dataclasses import dataclass, asdict
@dataclass
class Address:
city: str
zipcode: str
@dataclass
class User:
name: str
age: int
address: Addressu = User("alice", 30, Address("์์ธ", "06000"))
print(asdict(u))
# {'name': 'alice', 'age': 30, 'address': {'city': '์์ธ', 'zipcode': '06000'}}์ค์ฒฉ๋ dataclass๋ ์ฌ๊ท์ ์ผ๋ก ๋ณํ๋๋ค. JSON ์ง๋ ฌํํ ๋ ์ ์ฉํ๋ค.
import json
json.dumps(asdict(u), ensure_ascii=False)
# '{"name": "alice", "age": 30, "address": {"city": "์์ธ", "zipcode": "06000"}}'astuple() โ ํํ ๋ณํ
from dataclasses import astuple
print(astuple(u))
# ('alice', 30, ('์์ธ', '06000'))replace() โ ์ผ๋ถ ํ๋๋ง ๋ฐ๊พผ ์ ์ธ์คํด์ค
์๋ณธ์ ์์ ํ์ง ์๊ณ ๋ณต์ฌ๋ณธ์ ๋ง๋ ๋ค. frozen=True์ธ dataclass์์ ํนํ ์ ์ฉํ๋ค.
from dataclasses import replace
@dataclass(frozen=True)
class Config:
host: str
port: int
debug: bool = Falseprod = Config("api.example.com", 443)
dev = replace(prod, host="localhost", port=8080, debug=True)
print(prod) # Config(host='api.example.com', port=443, debug=False)
print(dev) # Config(host='localhost', port=8080, debug=True)fields() โ ํ๋ ์ ๋ณด ์กฐํ
from dataclasses import fields
for f in fields(User):
print(f"name={f.name}, type={f.type}, default={f.default}")
# name=name, type=<class 'str'>, default=MISSING
# name=age, type=<class 'int'>, default=MISSING
# name=address, type=<class 'Address'>, default=MISSING์ง๋ ฌํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ORM์ ์ง์ ๋ง๋ค ๋ ํ๋ ๋ฉํ์ ๋ณด๋ฅผ ๋์ ์ผ๋ก ์ฝ๋ ์ฉ๋๋ก ์ด๋ค.
8. ์ธ์ ์ฐ๊ณ , ์ธ์ ์ ์ธ๊น
dataclass๊ฐ ์ ํฉํ ๊ฒฝ์ฐ
- ์ค์ /ํ๊ฒฝ ๊ฐ์ฒด โ DB ์ ์ ์ ๋ณด, API ์ค์ ๋ฑ ๊ตฌ์กฐํ๋ ๊ฐ
- API ์๋ต/์์ฒญ ๋งคํ โ JSON ์คํค๋ง๊ฐ ๊ณ ์ ๋ ๋ฐ์ดํฐ
- DTO (Data Transfer Object) โ ํจ์ ๊ฐ, ๋ ์ด์ด ๊ฐ ๋ฐ์ดํฐ ์ ๋ฌ
- ๋๋ฉ์ธ ๋ชจ๋ธ โ ๋น์ฆ๋์ค ๋ก์ง์ด ํฌํจ๋ ๊ฐ ๊ฐ์ฒด
- ํ ์คํธ ํฝ์ค์ฒ โ ํ ์คํธ ๋ฐ์ดํฐ๋ฅผ ๊น๋ํ๊ฒ ๊ตฌ์ฑ
# ์ค์ ๊ฐ์ฒด
@dataclass(frozen=True)
class RedisConfig:
host: str = "localhost"
port: int = 6379
db: int = 0
password: str | None = None
# API ์๋ต ๋งคํ
@dataclass
class PaginatedResponse:
items: list[dict]
total: int
page: int
per_page: int
has_next: bool = field(init=False)
def __post_init__(self):
self.has_next = self.page * self.per_page < self.totaldataclass๊ฐ ์๋ ๊ฒ ๋์ ๊ฒฝ์ฐ
| ์ํฉ | ๋ ๋์ ์ ํ | ์ด์ |
|---|---|---|
| ์คํค๋ง๊ฐ ์ ๋์ ์ธ JSON | dict | ํ๋๊ฐ ๋งค๋ฒ ๋ฌ๋ผ์ง๋ฉด dataclass ์ ์๊ฐ ์๋ฏธ ์์ |
| ๋ถ๋ณ + ํํ ์ธํฉ์ด ํ์ | NamedTuple | x, y = point ํจํด์ ์์ฃผ ์ธ ๋ |
| ๋ณต์กํ ๊ฒ์ฆ/๋ณํ ๋ก์ง | pydantic ๋๋ attrs | dataclass์ __post_init__๋ง์ผ๋ก ๋ถ์กฑํ ๋ |
| ๋จ์ 2~3๊ฐ ๊ฐ์ ์์๋ก ๋ฌถ์ ๋ | tuple ๋๋ dict | ํด๋์ค ์ ์ ์ค๋ฒํค๋๊ฐ ์คํ๋ ค ํผ |
| ORM ๋ชจ๋ธ | SQLAlchemy Model ๋ฑ | ORM์ ์์ฒด ๋ฉํํด๋์ค ์์คํ ์ด ์์ |
dataclass vs pydantic
pydantic์ ๋ฐํ์ ํ์ ๊ฒ์ฆ๊ณผ ์๋ ๋ณํ์ด ํต์ฌ์ด๋ค.
age: int์"30"(๋ฌธ์์ด)์ ๋ฃ์ผ๋ฉด pydantic์ ์๋์ผ๋กint(30)์ผ๋ก ๋ณํํ์ง๋ง, dataclass๋ ๊ทธ๋๋ก"30"์ด ๋ค์ด๊ฐ๋ค. API ์ ๋ ฅ ๊ฒ์ฆ์ด ํ์ํ๋ฉด pydantic, ๋ด๋ถ ๋ฐ์ดํฐ ๊ตฌ์กฐํ๋ง ํ์ํ๋ฉด dataclass๊ฐ ์ ํฉํ๋ค.
dataclass vs attrs
attrs(2015)๋ ์ฌ์ค dataclass(2017, PEP 557)๋ณด๋ค ๋จผ์ ๋์๊ณ , dataclass๊ฐ attrs์์ ์๊ฐ์ ๋ฐ์ ๋ง๋ค์ด์ก๋ค. attrs๋ validator, converter ๊ฐ์ ๊ธฐ๋ฅ์ ๊ธฐ๋ณธ ์ ๊ณตํด์
__post_init__์ผ๋ก ์ง์ ๊ตฌํํด์ผ ํ๋ ๊ฒ์ฆ/๋ณํ์ ์ ์ธ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋ค.from attrs import define, field from attrs.validators import gt, instance_of @define class User: name: str = field(validator=instance_of(str)) age: int = field(validator=gt(0)) User("alice", -1) # ValueError โ validator๊ฐ ์๋์ผ๋ก ๊ฒ์ฆdataclass๋ ํ์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ผ ์ค์น๊ฐ ํ์ ์๊ณ ๋๋ถ๋ถ์ ๊ฒฝ์ฐ ์ถฉ๋ถํ๋ค. ํ๋๋ณ ๊ฒ์ฆ/๋ณํ์ด ๋ณต์กํด์ง๋ฉด attrs๋ฅผ ๊ณ ๋ คํ๋ฉด ๋๋ค.