فصل دهم: مدل‌های مدرن داده در پایتون

Please login to bookmark Close

مقدمه

در فصل نهم، با ابزارهای انتزاع مانند اینترفیس‌ها، پروتکل‌ها و ژنریک‌ها آشنا شدید. این ابزارها به شما کمک می‌کنند تا قراردادهای محکمی برای کد خود تعریف کنید. اما در دنیای واقعی، بخش بزرگی از برنامه‌نویسی به مدل‌سازی داده اختصاص دارد. یعنی باید ساختارهایی تعریف کنید که داده‌های برنامه را در خود نگهداری کنند؛ مانند اطلاعات یک کاربر، یک محصول، یا یک سفارش.

پایتون ابزارهای مدرن و قدرتمندی برای این کار ارائه می‌دهد که نوشتن کدهای تکراری و مستعد خطا را به حداقل می‌رسانند. در این فصل، با چهار ابزار اصلی آشنا می‌شوید:

  1. dataclass: برای تعریف کلاس‌هایی که عمدتاً داده نگهداری می‌کنند.
  2. NamedTuple: برای تعریف تاپل‌هایی که هر عنصر آن یک نام دارد.
  3. Enum: برای تعریف مجموعه‌ای از ثابت‌های نام‌گذاری‌شده.
  4. TypedDict: برای تعیین نوع دقیق کلیدها و مقادیر در دیکشنری‌ها.

این ابزارها به شما کمک می‌کنند تا کد تمیزتر، خواناتر و ایمن‌تری بنویسید و بسیاری از اشکالات را پیش از اجرا شناسایی کنید.

چرا به مدل‌های مدرن داده نیاز داریم؟

فرض کنید می‌خواهید یک کلاس ساده برای نمایش یک کاربر تعریف کنید. روش سنتی به این شکل است:

class User:
    def __init__(self, name, email, age):
        self.name = name
        self.email = email
        self.age = age

user1 = User("Ali", "ali@site.com", 30)
print(user1.name)  # Ali

این روش برای کلاس‌های کوچک کار می‌کند، اما به محض اینکه تعداد ویژگی‌ها و متدها افزایش یابد، مشکلاتی ظاهر می‌شود:

  • نوشتن کد تکراری: برای هر ویژگی، باید پارامتر، مقداردهی، و احتمالاً متدهای __repr__ و __eq__ را بنویسید.
  • خوانایی پایین: برای درک ساختار کلاس، باید به بدنه‌ی __init__ نگاه کنید.
  • خطاهای انسانی: به راحتی ممکن است نام یک ویژگی را اشتباه تایپ کنید یا در مقداردهی آن دچار اشتباه شوید.

ابزارهای مدرن این مشکلات را حل می‌کنند. آنها به شما امکان می‌دهند تا با حداقل کد، کلاس‌های داده‌محور قدرتمند و ایمنی بسازید. بیایید هر یک را به ترتیب بررسی کنیم.

۱. dataclass

تعریف و مفهوم

dataclass یک دکوراتور است که از پایتون ۳.۷ به بعد اضافه شده است. با استفاده از آن، می‌توانید کلاسی تعریف کنید که به طور خودکار متدهای __init__، __repr__، __eq__ و… را بر اساس ویژگی‌هایی که تعریف می‌کنید، تولید کند. این کار، حجم کد تکراری را به شدت کاهش می‌دهد.

نحوه‌ی استفاده

برای استفاده از dataclass، باید آن را از ماژول dataclasses import کنید و به عنوان دکوراتور بالای کلاس قرار دهید. سپس ویژگی‌ها را با نوع‌دهی (Type Hint) تعریف کنید.

from dataclasses import dataclass

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

با این کد ساده، کلاس User به طور خودکار دارای موارد زیر خواهد بود:

  • یک متد __init__ که سه پارامتر name، email و age را می‌گیرد و در شیء ذخیره می‌کند.
  • یک متد __repr__ که نمایش خوانایی از شیء برمی‌گرداند.
  • یک متد __eq__ که دو شیء را بر اساس مقدار تمام ویژگی‌ها مقایسه می‌کند.
user1 = User("Ali", "ali@site.com", 30)
user2 = User("Ali", "ali@site.com", 30)
user3 = User("Sara", "sara@site.com", 25)

print(user1)          # User(name='Ali', email='ali@site.com', age=30)
print(user1 == user2) # True
print(user1 == user3) # False

همان‌طور که می‌بینید، با نوشتن تنها سه خط کد، یک کلاس کامل با متدهای ضروری ساخته شده است.

مزایا و کاربردهای پیشرفته

۱. تنظیم مقادیر پیش‌فرض
می‌توانید برای هر ویژگی یک مقدار پیش‌فرض تعیین کنید. توجه داشته باشید که ویژگی‌های با مقدار پیش‌فرض باید پس از ویژگی‌های بدون مقدار پیش‌فرض بیایند.

@dataclass
class Product:
    title: str
    price: float
    currency: str = "IRR"       # default value
    quantity: int = 1           # default value

p1 = Product("Laptop", 1200.0)
p2 = Product("Book", 15.0, "USD", 2)
print(p1)  # Product(title='Laptop', price=1200.0, currency='IRR', quantity=1)

۲. ویژگی‌های محاسبه‌شونده (با __post_init__)
گاهی می‌خواهید پس از مقداردهی اولیه، یک ویژگی جدید بر اساس ویژگی‌های دیگر محاسبه کنید. برای این کار از متد جادویی __post_init__ استفاده می‌شود. این متد بلافاصله پس از __init__ اجرا می‌شود.

from dataclasses import dataclass, field

@dataclass
class Rectangle:
    width: float
    height: float
    area: float = field(init=False)   # This field is not in __init__

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

rect = Rectangle(4, 5)
print(rect)            # Rectangle(width=4, height=5, area=20)
print(rect.area)       # 20

در این مثال، area به عنوان یک ویژگی محاسبه‌شونده تعریف شده است و کاربر نمی‌تواند آن را در سازنده مقداردهی کند.

۳. نوع‌های قابل تغییر و غیرقابل تغییر
به طور پیش‌فرض، اشیاء dataclass قابل تغییر (mutable) هستند. اما می‌توانید با تنظیم frozen=True در دکوراتور، آنها را غیرقابل تغییر (immutable) کنید. این کار برای ایمنی در محیط‌های چندنخی یا به عنوان کلید دیکشنری مفید است.

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

p = ImmutablePoint(1, 2)
# p.x = 5  # dataclasses.FrozenInstanceError: cannot assign to field 'x'

۴. مرتب‌سازی و مقایسه
با تنظیم order=True، پایتون به طور خودکار متدهای مقایسه (__lt__, __le__, __gt__, __ge__) را بر اساس ترتیب تعریف ویژگی‌ها تولید می‌کند.

@dataclass(order=True)
class Person:
    name: str
    age: int

p1 = Person("Ali", 30)
p2 = Person("Sara", 25)
print(p1 > p2)   # True (because 'Ali' > 'Sara' lexicographically)

در کد بالا، مقایسه ابتدا بین name ها و سپس بین age ها انجام می‌شود. اما چون Ali از نظر ترتیب الفبا از Sara جلوتر است بزرگتر تشخیص داده شده است.

@dataclass(order=True)
class Person:
    name: str
    age: int

p1 = Person("Ali", 30)
p2 = Person("Ali", 25)

print(p1 > p2)  # True (چون name برابر بود، age رو مقایسه کرد: 30 > 25)

در کد بالا ابتدا مقایسه بین نام ها انجام می‌شود و چون نام ها برابرند سپس مقایسه بین سن ها انجام می‌شود. در واقع مقایسه بین ویژگی ها به ترتیبی که نوشته شده اند انجام می‌شود.

برای کنترل دقیق‌تر ترتیب، می‌توانید از پارامتر field(order=False) استفاده کنید تا یک ویژگی از مقایسه حذف شود.

from dataclasses import dataclass, field

@dataclass(order=True)
class Person:
    age: int = field(compare=True)
    name: str = field(compare=False)

در تکه کد بالا، مقایسه فقط بین سن ها انجام می‌شود.

چه زمانی از dataclass استفاده کنیم؟

dataclass بهترین گزینه برای اکثر کلاس‌هایی است که صرفاً داده نگهداری می‌کنند و رفتار پیچیده‌ای ندارند. اگر کلاس شما نیاز به متدهای سفارشی زیادی دارد، یا باید از یک کلاس پایه خاص ارث‌ببرد، شاید کلاس معمولی گزینه‌ی مناسب‌تری باشد. با این حال، برای مدل‌های داده در برنامه‌های کاربردی، وب‌سرویس‌ها و تحلیل داده، dataclass انتخابی عالی و مدرن است.

۲. NamedTuple (تاپل نام‌گذاری‌شده)

تعریف و مفهوم

یک NamedTuple یک تاپل (tuple) با ویژگی‌های اضافی است: هر عنصر آن یک نام دارد. این یعنی می‌توانید هم مانند یک تاپل به عناصر با ایندکس دسترسی داشته باشید، هم مانند یک شیء با نام ویژگی.

NamedTupleها از پایتون ۳.۵ به بعد در ماژول typing وجود دارند و از پایتون ۳.۶ به بعد، یک روش جدیدتر و خواناتر نیز ارائه شده است.

نحوه‌ی استفاده و مثال‌ها

روش سنتی تعریف NamedTuple با استفاده از تابع NamedTuple به این صورت است (این روش در نسخه‌های قدیمی‌تر پایتون رایج بود):

from typing import NamedTuple

Point = NamedTuple('Point', [('x', int), ('y', int), ('label', str)])

اما روش مدرن‌تر، خواناتر و توصیه‌شده در پایتون ۳.۶ به بعد، استفاده از سینتکس کلاس و ارث‌بری از NamedTuple است:

from typing import NamedTuple

class Point(NamedTuple):
    x: int
    y: int
    label: str = "unknown"

p = Point(10, 20)
print(p)                # Point(x=10, y=20, label='unknown')
print(p.x)              # 10 (access by name)
print(p[0])             # 10 (access by index like a tuple)

این سینتکس کلاسی، با سایر ابزارهای مدرن پایتون (مانند dataclass) هماهنگ‌تر است و توسط ابزارهای تحلیل ایستا (مانند mypy) به خوبی پشتیبانی می‌شود. همچنین خوانایی و نگهداری کد را بهبود می‌بخشد.

تفاوت‌ها و کاربردها در مقایسه با dataclass

ویژگی اصلی NamedTuple این است که غیرقابل تغییر (immutable) است. این برخلاف dataclass پیش‌فرض است که قابل تغییر است. همچنین NamedTupleها از نظر حافظه و سرعت، کمی بهینه‌تر از dataclassها هستند و با تاپل‌های معمولی سازگاری کامل دارند.

ویژگیdataclass (پیش‌فرض)NamedTuple
تغییرپذیریقابل تغییر (mutable)غیرقابل تغییر (immutable)
عملکردکمی کندتر (به دلیل داینامیک بودن)کمی سریع‌تر (ساختار ساده‌تر)
ارث‌بریاز هر کلاسی می‌تواند ارث‌ببردفقط از tuple ارث می‌برد
متدهای سفارشیبه راحتی قابل اضافه‌کردن استمحدودتر است
موارد استفادهمدل‌های داده‌ای که ممکن است تغییر کنندکلیدهای دیکشنری، داده‌های ثابت، استفاده در مجموعه‌ها

چه زمانی از NamedTuple استفاده کنیم؟

  • وقتی به یک ساختار داده‌ی ساده، سبک و غیرقابل تغییر نیاز دارید.
  • وقتی می‌خواهید از آن به عنوان کلید در دیکشنری یا عضو در مجموعه (set) استفاده کنید (چون قابل هش است).
  • وقتی عملکرد برای شما بسیار مهم است و ساختار داده‌ی شما بسیار ساده است.

مثال کلاسیک، استفاده از NamedTuple برای مختصات جغرافیایی یا مقادیر ثابتی است که تغییر نمی‌کنند.

class Coordinate(NamedTuple):
    lat: float
    lon: float

home = Coordinate(35.6892, 51.3890)
# home.lat = 36.0  # AttributeError: can't set attribute

۳. Enum (شمارنده‌ها)

تعریف و مفهوم

Enum (مخفف Enumeration) یک نوع داده است که به شما اجازه می‌دهد مجموعه‌ای از ثابت‌های نام‌گذاری‌شده را تعریف کنید. این ثابت‌ها معمولاً مقادیری هستند که تنوع محدودی دارند و در کد به عنوان گزینه‌های مشخص استفاده می‌شوند.

پیش از Enum، برنامه‌نویسان اغلب از اعداد یا رشته‌ها برای این کار استفاده می‌کردند که منجر به کدهای شکننده و خطاهای ناشی از اشتباهات تایپی می‌شد. Enum این مشکل را با ایجاد یک نوع امن حل می‌کند.

نحوه‌ی تعریف و استفاده

برای تعریف یک Enum، باید از کلاس پایه Enum در ماژول enum ارث‌ببرید و ثابت‌ها را به صورت attributeهای کلاس تعریف کنید.

from enum import Enum

class OrderStatus(Enum):
    PENDING = "pending"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

حالا می‌توانید از این Enum در کد خود استفاده کنید:

# Accessing members
print(OrderStatus.PENDING)          # OrderStatus.PENDING
print(OrderStatus.PENDING.value)    # pending
print(OrderStatus.PENDING.name)     # PENDING

# Iterating over all members
for status in OrderStatus:
    print(status)   # OrderStatus.PENDING, OrderStatus.SHIPPED, ...

مزیت بزرگ، ایمنی نوع است. اگر تابعی یک OrderStatus را به عنوان پارامتر بپذیرد، شما نمی‌توانید یک رشته یا عدد دلخواه به آن بدهید؛ فقط یکی از اعضای Enum معتبر است.

def update_order(status: OrderStatus):
    if status == OrderStatus.SHIPPED:
        print("Order is shipped.")
    elif status == OrderStatus.DELIVERED:
        print("Order is delivered.")

update_order(OrderStatus.SHIPPED)   # Correct
# update_order("shipped")           # Type-checker error

کاربردهای پیشرفته: مقایسه و اعضای خودکار

۱. مقایسه‌ها
اعضای Enum را می‌توان با یکدیگر مقایسه کرد.

class Priority(Enum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3

print(Priority.HIGH > Priority.LOW)   # True

۲. اعضای خودکار با auto()
اگر به مقدار خاصی برای Enumها نیاز ندارید، می‌توانید از تابع auto() استفاده کنید تا پایتون به طور خودکار یک مقدار منحصربه‌فرد به آنها اختصاص دهد.

from enum import Enum, auto

class Color(Enum):
    RED = auto()
    GREEN = auto()
    BLUE = auto()
    # Values will be 1, 2, 3 respectively

۳. ایجاد پویای Enum
می‌توانید به‌صورت پویا Enum ایجاد کنید.

Status = Enum('Status', ['PENDING', 'SHIPPED', 'DELIVERED'])
print(Status.PENDING)   # Status.PENDING

چه زمانی از Enum استفاده کنیم؟

Enumها برای هر جایی که مجموعه‌ای از گزینه‌های محدود و مشخص دارید، بسیار مناسب هستند. به عنوان مثال:

  • وضعیت یک سفارش (در انتظار، ارسال‌شده، تحویل‌داده‌شده)
  • نقش‌های کاربر (مدیر، کاربر عادی، مهمان)
  • روزهای هفته
  • نوع‌های مختلف یک رویداد

استفاده از Enum باعث می‌شود کد شما خودمستندتر شود و از بروز خطاهای ناشی از استفاده از رشته‌ها یا اعداد جادویی (Magic Numbers) جلوگیری کنید.

منظور از Magic Numbers چیست؟

اعدادی که در کد دیده می‌شوند ولی هیچ توضیحی در موردشان وجود ندارد و خواننده باید حدس بزند که این چیست.

# ❌ بد: Magic Number
if total_price > 1000:
    discount = total_price * 0.1
    print("Discount applied!")

در کد بالا 1000 یعنی چی؟ حداقل خرید برای تخفیف؟ و 0.1 یعنی چی؟ ۱۰ درصد تخفیف؟ این اعداد که معنی خاصی برایشان قائل نشده اند و خواننده باید خودش بفهمد که منظور چیست را Magic Numbers می‌گویند. نسخه اصلاح شده کد بالا به شرح زیر است.

# ✅ خوب: با اسم مشخص
MIN_ORDER_FOR_DISCOUNT = 1000
DISCOUNT_RATE = 0.1

if total_price > MIN_ORDER_FOR_DISCOUNT:
    discount = total_price * DISCOUNT_RATE
    print("Discount applied!")

۴. TypedDict (دیکشنری نوع‌دار)

تعریف و مفهوم

TypedDict یک ابزار در ماژول typing است که به شما اجازه می‌دهد شکل (Shape) یک دیکشنری را مشخص کنید. یعنی تعیین کنید که یک دیکشنری باید چه کلیدهایی داشته باشد و مقدار هر کلید از چه نوعی باشد.

این ابزار در زمان اجرا هیچ تأثیری ندارد و صرفاً برای ابزارهای تحلیل ایستای کد (مانند mypy) طراحی شده است تا بتوانند اشتباهات مربوط به کلیدها و مقادیر دیکشنری را پیش از اجرا شناسایی کنند. این امکان در پایتون ۳.۸ معرفی شده است.

نحوه‌ی تعریف و استفاده

برای تعریف یک TypedDict، از کلاس TypedDict در ماژول typing ارث‌ببرید و کلیدها و نوع‌های آنها را به صورت annotation های کلاس تعریف کنید.

from typing import TypedDict

class UserDict(TypedDict):
    name: str
    email: str
    age: int

حالا ابزارهای تایپ‌چک می‌دانند که یک دیکشنری از نوع UserDict باید دارای کلیدهای name، email و age با نوع‌های مشخص باشد.

def get_user_info(user: UserDict) -> str:
    return f"{user['name']} ({user['email']})"

valid_user: UserDict = {"name": "Ali", "email": "ali@site.com", "age": 30}
print(get_user_info(valid_user))  # Ali (ali@site.com)

# The following would cause a type-checker error:
# invalid_user: UserDict = {"name": "Sara"}  # missing 'email' and 'age'

نکته‌ی مهم: TypedDict یک کلاس واقعی نیست که نمونه‌سازی شود. شما همچنان از دیکشنری‌های معمولی استفاده می‌کنید، اما ابزارهای تایپ‌چک می‌توانند صحت آنها را بررسی کنند.

مزایا و محدودیت‌ها

مزایا:

  • مستندسازی عالی: مشخص می‌کند یک دیکشنری در کد شما چه ساختاری باید داشته باشد.
  • ایمنی در برابر خطاهای تایپی: تایپ‌چک می‌تواند اشتباهات مانند user['emai'] را بگیرد.
  • سازگاری با داده‌های JSON: دیکشنری‌ها رایج‌ترین ساختار برای تبادل داده (مثلاً در APIها) هستند و TypedDict به شما کمک می‌کند تا آنها را به طور ایمن مدیریت کنید.

محدودیت‌ها:

  • فقط برای تحلیل ایستا: در زمان اجرا، پایتون هیچ‌گونه اجباری برای رعایت ساختار اعمال نمی‌کند.
  • تایپ‌های پیچیده‌تر: می‌توانید از Optional، Union یا حتی TypedDictهای تو در تو استفاده کنید، اما تعریف آنها می‌تواند کمی پیچیده شود.

چه زمانی از TypedDict استفاده کنیم؟

TypedDict وقتی بسیار مفید است که با داده‌های نیمه‌ساختاریافته مانند داده‌های JSON که از APIها دریافت می‌کنید یا به دیتابیس‌های NoSQL می‌فرستید، کار می‌کنید. اگرچه می‌توانید این داده‌ها را به dataclass تبدیل کنید، اما در مواقعی که به یک نمایش ساده‌تر و انعطاف‌پذیرتر از داده نیاز دارید (مثلاً برای اعتبارسنجی سریع)، TypedDict انتخاب بسیار مناسبی است.

خلاصه و مقایسه‌ی ابزارها

در این فصل، با چهار ابزار قدرتمند برای مدل‌سازی داده در پایتون آشنا شدید. هر کدام جایگاه و کاربرد خاص خود را دارند. جدول زیر به شما کمک می‌کند تا تصمیم بگیرید در چه موقعیتی از هر کدام استفاده کنید.

ابزارویژگی اصلیبهترین کاربرد
dataclassکلاس داده‌ای با کد کممدل‌های اصلی برنامه که داده و رفتار ساده دارند.
NamedTupleتاپل غیرقابل تغییر با نام‌هاکلیدهای دیکشنری، داده‌های ثابت و سبک.
Enumمجموعه‌ای از ثابت‌های نام‌گذاری‌شدهگزینه‌های محدود (وضعیت‌ها، نوع‌ها، نقش‌ها).
TypedDictشمای نوع‌دار برای دیکشنری‌هاداده‌های JSON، APIها، و ساختارهای پویا.

با استفاده از این ابزارها، کد شما نه تنها تمیزتر و خواناتر می‌شود، بلکه با بهره‌گیری از تایپ‌هینترها، از بسیاری از خطاهای رایج در زمان اجرا نیز جلوگیری خواهد کرد.

پرسش‌های متداول (FAQ)

  • آیا می‌توانم از dataclass با NamedTuple ترکیب استفاده کنم؟ خیر، این دو رویکرد متفاوتی دارند و نمی‌توان آنها را مستقیماً ترکیب کرد، اما هر کدام جایگاه خاص خود را دارند.
  • آیا TypedDict در زمان اجرا خطا می‌دهد؟ خیر، صرفاً یک ابزار برای تحلیل ایستا است و در زمان اجرا هیچ اجباری اعمال نمی‌کند.
  • آیا می‌توان Enumها را به عدد یا رشته تبدیل کرد؟ بله، از طریق value و name به مقادیر زیرین دسترسی دارید.
  • کدام یک برای مدل‌های دیتابیس مناسب‌تر است؟ معمولاً dataclass به دلیل قابلیت تغییر و اضافه‌کردن متدهای سفارشی، انتخاب رایج‌تری برای مدل‌های دیتابیس است.

Please login to bookmark Close
نظرات

دیدگاهتان را بنویسید

فهرست مطالب

سرفصل دوره

تمرین

این قسمت تمرین ندارد!

پاسخ تمرین ها

هنوز برای تمرین‌های این قسمت پاسخی ثبت نشده است!

اشتراک گذاری

چرا بهتره از فیلترشکن استفاده کنید؟

من همه ویدئو ها و پادکست های کُدباز رو توی یوتیوب و ساندکلود و پلتفرم هایی آپلود می‌کنم که اغلب فیلتر هستند.

اغلب آموزش‌ها ویدئو و پادکست دارند. پس اگر می‌خواهید از محتوای سایت بیشترین استفاده رو ببرید نیاز به فیلتر شکن دارید.

توجه داشته باشید که برای خرید از فروشگاه بهتره فیلتر شکن رو خاموش کنید.

تنظیمات

انتخاب زبان
تغییر تم