مقدمه
در فصل نهم، با ابزارهای انتزاع مانند اینترفیسها، پروتکلها و ژنریکها آشنا شدید. این ابزارها به شما کمک میکنند تا قراردادهای محکمی برای کد خود تعریف کنید. اما در دنیای واقعی، بخش بزرگی از برنامهنویسی به مدلسازی داده اختصاص دارد. یعنی باید ساختارهایی تعریف کنید که دادههای برنامه را در خود نگهداری کنند؛ مانند اطلاعات یک کاربر، یک محصول، یا یک سفارش.
پایتون ابزارهای مدرن و قدرتمندی برای این کار ارائه میدهد که نوشتن کدهای تکراری و مستعد خطا را به حداقل میرسانند. در این فصل، با چهار ابزار اصلی آشنا میشوید:
- dataclass: برای تعریف کلاسهایی که عمدتاً داده نگهداری میکنند.
- NamedTuple: برای تعریف تاپلهایی که هر عنصر آن یک نام دارد.
- Enum: برای تعریف مجموعهای از ثابتهای نامگذاریشده.
- 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به دلیل قابلیت تغییر و اضافهکردن متدهای سفارشی، انتخاب رایجتری برای مدلهای دیتابیس است.