آنچه در این مقاله میخوانید [پنهانسازی]
برای ساخت مدل داده تمیز و قابل نگهداری، سادهترین راه در پایتون استفاده از dataclass است: فیلدها را با تایپ مشخص تعریف میکنید، @dataclass خودکار سازنده، مقایسه و نمایش شی را میسازد و با __post_init__ اعتبارسنجی انجام میدهید. با default_factory از خطای مقادیر پیشفرض قابل تغییر دور میمانید و با frozen مدلهای تغییرناپذیر میسازید. اینها هسته استفاده حرفهای از dataclass در پایتون هستند.
dataclass چیست و کی به درد میخورد؟
dataclass یک decorator در پایتون است که روی کلاسهای معمولی اعمال میشود و متدهای تکراری مثل __init__، __repr__، __eq__ و در صورت نیاز مرتبسازی را خودکار تولید میکند. هرجا «داده ساختیافته» دارید (مدل دامنه، DTO، پیکربندی، پیام رویداد)، dataclass کد شما را کوتاهتر و خواناتر میکند، بدون این که از امکانات کلاسهای عادی (متدها، ارثبری، تایپ هینت) محروم شوید.
الگوی پایه با یک مثال کوتاه
هدف: تعریف یک مدل ساده با مقدار پیشفرض و نمایش خوانا.
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int = 0 # مقدار پیش فرض
p = Person("Ali", 30)
print(p) # Person(name='Ali', age=30)
print(p.name) # Ali
بدون نوشتن سازنده، شی ساخته شد؛ __repr__ خوانا تولید شد؛ برابر بودن دو شخص با مقایسه فیلدها انجام میشود.
ساخت یک «مدل داده تمیز»: اصول و الگوی پیشنهادی
یک مدل تمیز فقط اسم فیلدها نیست؛ باید رفتار، اعتبارسنجی و ایمنی را هم پوشش دهید.
۱) فیلدهای صریح با تایپ هینت
بدون تایپ هینت، کارایی dataclass نصف میشود. تایپها هم خوانایی را بالا میبرند هم در ابزارهای استاتیک مانند mypy/pyright کمک میکنند.
۲) اجتناب از پیشفرضهای قابل تغییر
بهجای list یا dict خالی بهعنوان پیشفرض، از default_factory استفاده کنید.
from dataclasses import dataclass, field
from typing import List
@dataclass
class TagSet:
article_id: int
tags: List[str] = field(default_factory=list) # درست: نمونه جدید برای هر شی
۳) اعتبارسنجی در __post_init__
__post_init__ بعد از __init__ اجرا میشود؛ بهترین جا برای کنترل ورودی است.
from dataclasses import dataclass, field
from datetime import date
@dataclass(frozen=True)
class User:
id: int
email: str
joined_at: date = field(default_factory=date.today)
is_active: bool = True
def __post_init__(self):
if "@" not in self.email:
raise ValueError("invalid email")
با frozen=True شی تغییرناپذیر میشود. تا وقتی فقط «اعتبارسنجی» میکنید نیازی به تغییر فیلد نیست، اما اگر normalization میخواهید باید از object.__setattr__ استفاده کنید.
def __post_init__(self):
normalized = self.email.strip().lower()
if "@" not in normalized:
raise ValueError("invalid email")
# چون کلاس frozen است:
object.__setattr__(self, "email", normalized)
۴) متدهای دامنهای؛ محاسبه را بیرون از سازنده نگه دارید
بهجای پر منطق کردن سازنده، محاسبات را در ویژگیها یا متدها نگه دارید.
from dataclasses import dataclass
@dataclass(frozen=True)
class Money:
amount: int # به ریال
currency: str = "IRR"
def with_tax(self, percent: float) -> "Money":
taxed = int(self.amount * (1 + percent))
return Money(taxed, self.currency)
یک مثال عملی: مدل سفارش ساده با اعتبارسنجی و سریالسازی
سناریو: سفارش فروشگاهی با اقلام تو در تو، تخفیف و محاسبه مجموع.
from dataclasses import dataclass, field, asdict
from typing import List
@dataclass(frozen=True)
class Item:
name: str
price: int # ریال
qty: int = 1
def __post_init__(self):
if self.price < 0:
raise ValueError("price cannot be negative")
if self.qty < 1:
raise ValueError("qty must be >= 1")
@dataclass
class Order:
id: str
items: List[Item] = field(default_factory=list)
discount: float = 0.0 # بین 0 و 1
def __post_init__(self):
if not (0.0 <= self.discount < 1.0):
raise ValueError("discount must be in [0,1)")
def add_item(self, item: Item) -> None:
self.items.append(item)
@property
def total(self) -> int:
subtotal = sum(i.price * i.qty for i in self.items)
return int(subtotal * (1 - self.discount))
def to_dict(self) -> dict:
# asdict به صورت بازگشتی تمام dataclass های تو در تو را تبدیل میکند.
return asdict(self)
@classmethod
def from_dict(cls, data: dict) -> "Order":
raw_items = data.get("items", [])
items = [Item(**it) for it in raw_items]
return cls(id=data["id"], items=items, discount=data.get("discount", 0.0))
# استفاده
order = Order(id="A-1001", discount=0.1)
order.add_item(Item("Book", price=200_000, qty=2))
order.add_item(Item("Pen", price=30_000))
print(order.total) # 387000
print(order.to_dict()) # {'id': 'A-1001', 'items': [...], 'discount': 0.1}
روش بررسی نتیجه: مجموع را دستی حساب کنید و با total مقایسه کنید؛ تلاش برای ثبت قیمت منفی باید خطا دهد؛ سریالسازی dict باید ساختار منظم و قابل JSON داشته باشد.
ترفندها و نکات رفتاری مهم dataclass
- برابری و هش: اگر eq=True و frozen=True باشند، hash بهطور ایمن قابل تولید است و شی میتواند کلید دیکشنری یا عضو set باشد. اگر شی قابل تغییر است، روی hash تکیه نکنید.
- مرتبسازی: با order=True مقایسه بر اساس ترتیب فیلدها انجام میشود. اگر فقط برخی فیلدها باید مقایسه شوند، برای بقیه از field(compare=False) استفاده کنید.
- پنهان کردن فیلدها در نمایش: field(repr=False) فیلدهای حساس (مثل توکن) را از __repr__ حذف میکند.
- خروج از سازنده: field(init=False) برای مقادیری که در __post_init__ پر میشوند مناسب است (مثل slug یا cache).
- metadata: میتوانید با field(metadata={“unit”: “IRR”}) متادیتا ذخیره کنید؛ برای ابزارهای بیرونی یا اعتبارسنجی سفارشی مفید است.
- slots و صرفهجویی حافظه: اگر مفسر شما از پارامتر slots در dataclass پشتیبانی کند، با slots=True خصیصهها در __slots__ تعریف میشوند و مصرف حافظه کاهش مییابد. قبل از استفاده، سازگاری نسخه پایتون پروژه را بررسی کنید.
- ارثبری: dataclass از ارثبری پشتیبانی میکند، اما ترتیب فیلدها در کلاسهای فرزند مهم است. فیلدهای بدون پیشفرض باید قبل از فیلدهای دارای پیشفرض بیایند.
الگوی تبدیل به dict/JSON و نکات سریالسازی
asdict به شکل بازگشتی dataclass ها را به dict تبدیل میکند و برای JSON کافی است. توجه کنید:
- ویژگیها و property ها به dict نمیآیند؛ فقط فیلدها سریال میشوند. اگر لازم است، دستی اضافه کنید.
- انواع غیرقابل JSON (مثل date) را قبل از json.dumps به str تبدیل کنید یا در __post_init__/متد سریالسازی، تبدیل انجام دهید.
import json
from dataclasses import asdict, dataclass, field
from datetime import date
@dataclass
class Profile:
id: int
birthday: date = field(default_factory=date.today)
def to_json(self) -> str:
d = asdict(self)
d["birthday"] = d["birthday"].isoformat()
return json.dumps(d, ensure_ascii=False)
print(Profile(1).to_json()) # {"id": 1, "birthday": "2026-08-30"}
اشتباهات رایج و راهحل آنها
- پیشفرضهای قابل تغییر مشترک میشوند: بهجای tags=[] از field(default_factory=list) استفاده کنید.
- توقع «تبدیل خودکار» انواع: dataclass داده را نگه میدارد، تبدیل/اعتبارسنجی با شماست. در __post_init__ تبدیل کنید (مثلا str به int).
- استفاده از hash روی مدلهای قابل تغییر: ممکن است رفتار مجموعهها را خراب کند. اگر لازم دارید، مدل را frozen کنید یا hash سفارشی ایمن بسازید.
- مرتبسازی ناخواسته: فعال کردن order=True روی فیلدهایی که نباید مقایسه شوند میتواند باگ بسازد. فیلدهای غیرمرتبط را compare=False کنید.
- تغییر در __post_init__ روی کلاس frozen: مستقیم انتساب نکنید؛ از object.__setattr__ استفاده کنید.
چه زمانی سراغ گزینه دیگری برویم؟
- ساختار بسیار سبک و فقط تاپلمانند: namedtuple جواب میدهد و ذاتا تغییرناپذیر است.
- اگر صرفا «دیکشنری تایپشده» میخواهید برای تبادل داده با کتابخانهها: TypedDict مناسب تایپهینت است.
- برای اعتبارسنجی قوی، تبدیل نوع از ورودیهای متنی/شبکه و پیام خطای غنی: از کتابخانههای تخصصی اعتبارسنجی/مدلسازی داده استفاده کنید.
چک لیست اجرایی سریع
- فیلدها را با تایپ دقیق بنویسید؛ نامها را معنادار انتخاب کنید.
- برای list/dict/set از default_factory استفاده کنید.
- اعتبارسنجی و normalization را در __post_init__ پیاده کنید.
- اگر شی باید کلید دیکشنری باشد، frozen=True را فعال کنید.
- برای سریالسازی، asdict و تبدیل انواع خاص (مثل date) را پیاده کنید.
- فیلدهای حساس را با repr=False از نمایش عمومی حذف کنید.
- در صورت نیاز به حافظه کمتر، قابلیت slots را در نسخه سازگار فعال کنید.
گام بعدی چیست؟
یکی از مدلهای فعلی پروژه را انتخاب کنید و آن را به dataclass تبدیل کنید. از default_factory و __post_init__ استفاده کنید، تستی برای برابری اشیا و سریالسازی بنویسید و سپس در صورت نیاز سراغ frozen و (در نسخه سازگار) slots بروید. این تغییر کوچک، یکدستی و کیفیت مدلهای داده شما را بالا میبرد.







