dataclass در پایتون 3.7 به عنوان ابزاری کاربردی معرفی شد که برای ساخت کلاسهای ساختاریافته مخصوص ذخیرهسازی دادهها مورد استفاده قرار میگیرد. این کلاسها دارای ویژگیها و توابع خاصی هستند که به طور خاص با دادهها و نمایش آنها در ارتباط هستند.
کدنویسی به شیوه سخت!
به عنوان یک برنامهنویس پایتون تاکنون چند مرتبه تصمیم گرفتهاید انواع مختلفی از دادهها یا یک ساختار دادهای پیچیده را با استفاده از یک تابع پایتون به عنوان خروجی بازگردانید و در نهایت از یک دیکشنری (dictionary) یا لیست (List) استفاده کردهاید؟ قطعه کد زیر مثال سادهای در این زمینه را نشان میدهد.
def get_center_of_rectangle(topx, topy, bottomx, bottomy):
.... Logic Here ...
return pointx, pointy
// OR
def get_center_of_rectangle(topx, topy, bottomx, bottomy):
.... Logic Here ...
return {
"x": pointx,
"y": pointy
}
قطعه کد فوق بدون مشکل کار میکند، اما تصور کنید که این تابع قرار است درون توابع دیگری فراخوانی شود. در این حالت با مشکلات زیر روبرو میشوید:
1. باید امضای تابع (function signature) را اصلاح کنید.
2. پشتیبانی از ماژول typings محدود است. ما اغلب به این پشتیبانی برای تکمیل خودکار و تستهای ایستا نیاز داریم.
در زبان برنامهنویسی C برنامهنویسان به نوع دادهی کارآمدی که ساختار (Struct) نام دارد، دسترسی دارند که با کمک آن میتوان نوع داده بازگشتی را تعریف کرد. برنامهنویسان پایتون نیز قادر به انجام اینکار با استفاده از کلاسهای پایتون هستند، اما برای تعریف یک ساختار، باید کدهای زیادی بنویسند. در قطعه کد زیر نحوهی تعریف یک ساختار دادهای در زبان برنامهنویسی C را مشاهده میکنید که کوتاه و صریح است.
struct Person {
char name[50];
int citNo;
float salary;
};
در قطعه کد زیر کلاس پایتون را مشاهده میکنید که به نسبت زبان برنامهنویسی C به کدنویسی بیشتری نیاز دارید.
class Person:
name: str
citNo: int
salary: float
# In Code:
p = Person()
p.name = "Charanjit"
p.citNo = 1
p.salary = 0.0001
return p
مقدمهای بر dataclass در پایتون
پایتون نسخه 3.7 نوع دادهای جالب و البته کاربردی در اختیار پایتونیستها قرار داد که کلاس داده (dataclass) نام دارد. کلاس داده به شما این امکان را میدهد تا کلاسهایی با کدنویسی کمتر، اما قابلیتهای کاربردی بیشتر تعریف کنید. به بیان دقیقتر، کلاس داده، همانگونه که از نامش پیدا است، در بیشتر موارد شامل داده است، هرچند محدودیتی در این زمینه وجود ندارد.
قطعه کد زیر یک کلاس عادی به نام Person را تعریف میکند که دو ویژگی با نامهای name و age دارد.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
کلاس Person دارای متد __init__ است که ویژگیهای name و age را مقداردهی اولیه میکند.
اگر در نظر دارید رشتهای از شی Personرا نشان دهید، باید متد __str__ یا __repr__ را پیادهسازی کنید. همچنین، اگر در نظر دارید دو نمونه از کلاس Person را با یک ویژگی مقایسه کنید، باید متد __eq__ را پیادهسازی کنید.
با این حال، اگر از کلاس داده استفاده کنید، همه این ویژگیها (و حتی موارد بیشتر) را بدون نیاز به پیادهسازی این روشهای نسبتا وقتگیر در اختیار خواهید داشت.
دکوراتور (decorator) چیست؟
دکوراتور با هدف بهبود عملکرد کدها در اختیار توسعهدهندگان قرار دارد. دکوراتورها به توسعهدهندگان اجازه میدهند به توسعه و تغییر رفتار یک شی قابل فراخوانی بپردازند. البته اگر بخواهیم کمی دقیقتر و فنی صحبت کنیم، باید بگوییم که دکوراتورها مکانیزمی هستند که بر مبنای آنها یک بخش از برنامه سعی میکند تا بخش دیگری را در زمان اجرا (runtime) ویرایش کند. برای آنکه بتوانید از مکانیزم فوق بدون مشکل استفاده کنید، باید در ارتباط با مفاهیم زیربنایی پایتون اطلاعات کافی داشته باشید. اولین نکتهای که باید به آن دقت کنید این است که همه چیز در پایتون مثل کلاسها، توابع و.... شی (Object) به شمار میروند.
چگونه یک کلاس عادی را به یک کلاس داده تبدیل کنیم؟
برای تبدیل کلاس Person به یک کلاس داده، باید بر مبنای مراحل زیر گام بردارید.
1. ابتدا دکوراتور (decorator) کلاس داده را از ماژول کلاس داده به برنامه خود وارد (Import) کنید.
from dataclasses import dataclass
2. کلاس Person را با دکوراتور کلاس داده ویرایش کنید و ویژگیهای آن را تعریف کنید:
@dataclass
class Person:
name: str
age: int
3. در این مثال، کلاس Person دارای دو ویژگی نام با نوع str و age با نوع int است. با انجام این کار، دکوراتور dataclass@ به طور ضمنی متد __init__ را به صورت زیر ایجاد میکند:
def __init__(name: str, age: int)
4. توجه داشته باشید که ترتیب ویژگیهای تعریف شده در کلاس با ترتیب پارامترها در متد __init__ یکسان است. اکنون میتوانید شی Person را ایجاد کنید:
p1 = Person('John', 25)
5. هنگامی که شی Person را چاپ میکنید، مشاهده خواهید کرد به یک فرمت قابل خواندن دست پیدا میکنید:
print(p1)
خروجی قطعه کد فوق به صورت زیر است:
Person(name='John', age=25)
همچنین، اگر دو شی Person را با ویژگی یکسان مقایسه کنید، مقدار True بازگردانده میشود:
p1 = Person('John', 25)
p2 = Person('John', 25)
print(p1 == p2)
خروجی:
True
اکنون که با نحوهی تعریف کلاس داده آشنا شدید، وقت آن رسیده تا به سراغ توابعی برویم که یک کلاس داده در اختیارمان قرار میدهد.
مقادیر پیشفرض
هنگام استفاده از یک کلاس معمولی، میتوانید مقادیر پیشفرض را برای ویژگیها تعریف کنید. به عنوان مثال، کلاس Person زیر دارای پارامتر iq با مقدار پیشفرض 100 است.
class Person:
def __init__(self, name, age, iq=100):
self.name = name
self.age = age
self.iq = iq
برای تعریف مقدار پیشفرضی برای یک ویژگی در کلاس داده، باید آن را به این ویژگی اختصاص دهیم. قطعه کد زیر نحوهی انجام اینکار را نشان میدهد.
from dataclasses import dataclass
@dataclass
class Person:
name: str
age: int
iq: int = 100
print(Person('John Doe', 25))
شبیه به قوانینی که برای تعریف پارامترها به آن اشاره کردیم، ویژگیهای دارای مقادیر پیشفرض باید بعد از ویژگیهای بدون مقادیر پیشفرض ظاهر شوند. اگر این قاعده را رعایت نکنید با مشکل روبرو میشوید. به طور مثال، قطعه کد زیر کار نمیکند:
from dataclasses import dataclass
@dataclass
class Person:
iq: int = 100
name: str
age: int
تبدیل به یک Tuple
ماژول dataclasses دارای توابع astuple و asdict است که میتوانند نمونهای از کلاس داده را به تاپل و دیکشنری تبدیل کنند. قطعه کد زیر نحوهی انجام این کار را نشان میدهد:
from dataclasses import dataclass, astuple, asdict
@dataclass
class Person:
name: str
age: int
iq: int = 100
p = Person('John Doe', 25)
print(astuple(p))
print(asdict(p))
خروجی قطعه کد بالا به شرح زیر است:
('John Doe', 25, 100)
{'name': 'John Doe', 'age': 25, 'iq': 100}
چگونه اشیاء تغییرناپذیر (immutable) ایجاد کنیم؟
برای ساخت اشیا فقط خواندنی (Read Only) از یک کلاس داده، میتوانید آرگومان ثابت دکوراتور کلاس داده را روی True تنظیم کنید. قطعه کد زیر این موضوع را نشان میدهد:
from dataclasses import dataclass, astuple, asdict
@dataclass(frozen=True)
class Person:
name: str
age: int
iq: int = 100
در این حالت، اگر بعد از ساخت یک شی سعی کنید ویژگیهای آن را تغییر دهید، با پیغام خطای زیر روبرو میشوید:
p = Person('Jane Doe', 25)
p.iq = 120
پیغام خطای زیر به این نکته اشاره دارد که نمیتوان مقداری به فیلد iq اختصاص داد.
dataclasses.FrozenInstanceError: cannot assign to field 'iq'
سفارشیسازی رفتارهای یک ویژگی/خصلت
اگر نمیخواهید یک ویژگی را در متد __init__ مقداردهی اولیه کنید، میتوانید از تابع field از ماژول dataclasses استفاده کنید. مثال زیر ویژگی can_vote را تعریف میکند که با استفاده از متد __init__ مقداردهی اولیه شده است:
from dataclasses import dataclass, field
class Person:
name: str
age: int
iq: int = 100
can_vote: bool = field(init=False)
تابع ()field پارامترهای کاربردی و جالب توجهی مثل repr، hash، compare و metadata دارد که برنامه نویسان عاشق آن هستند.
اگر میخواهید یک ویژگی که وابسته به مقادیر ویژگی دیگری است را مقداردهی اولیه کنید، میتوانید از روش __post_init__ استفاده کنید. همانطور که از نامش قابل تشخیص است، پایتون متد __post_init__ را بعد از متد __init__ فراخوانی میکند.
قطعه کد زیر از متد __post_init__ برای مقداردهی اولیه ویژگی can_vote بر اساس ویژگی age استفاده میکند:
from dataclasses import dataclass, field
@dataclass
class Person:
name: str
age: int
iq: int = 100
can_vote: bool = field(init=False)
def __post_init__(self):
print('called __post_init__ method')
self.can_vote = 18 <= self.age <= 70
p = Person('Jane Doe', 25)
print(p)
خروجی قطعه کد بالا به شرح زیر است:
called the __post_init__ method
Person(name='Jane Doe', age=25, iq=100, can_vote=True)
مرتبسازی اشیا در کلاس داده پایتون
به طور پیشفرض، یک کلاس داده متد __eq__ را پیادهسازی میکند.
برای آنکه بتوانید از انواع مختلف مقایسهها مثل __lt__، __lte__، __gt__ و __gte__ استفاده کنید، میتوانید آرگومان ترتیب دکوراتور dataclass@ را روی مقدار True تنظیم کنید:
@dataclass(order=True)
با انجام این کار، کلاس داده اشیا را بر اساس هر فیلد مرتب میکند تا زمانی که مقداری غیر برابر پیدا کند.
در بیشتر موارد نیاز دارید تا اشیا را با یک ویژگی خاص و نه همهی ویژگیها مقایسه کنید. برای انجام این کار، باید فیلدی به نام sort_index تعریف کنید و مقدار آن را روی ویژگی که میخواهید مرتب کنید، تنظیم کنید.
به طور مثال، فرض کنید، فهرستی از اشیای Person دارید و میخواهید آنها را بر اساس سن مرتب کنید:
members = [
Person('John', 25),
Person('Bob', 35),
Person('Alice', 30)
]
برای انجام این کار، باید کارهای زیر را انجام دهید:
ابتدا پارامتر order=True را به دکوراتور dataclass@ ارسال کنید.
دوم، ویژگی sort_index را تعریف کرده و پارامتر init آن را روی False قرار دهید.
سوم، sort_index را روی ویژگی age در متد __post_init__ تنظیم کنید تا شی Person بر اساس سن مرتب شود.
قطعه کد زیر نشان میدهد که چگونه اشیا Person را بر مبنای سن افراد مرتب میکنیم:
from dataclasses import dataclass, field
@dataclass(order=True)
class Person:
sort_index: int = field(init=False, repr=False)
name: str
age: int
iq: int = 100
can_vote: bool = field(init=False)
def __post_init__(self):
self.can_vote = 18 <= self.age <= 70
# sort by age
self.sort_index = self.age
members = [
Person(name='John', age=25),
Person(name='Bob', age=35),
Person(name='Alice', age=30)
]
sorted_members = sorted(members)
for member in sorted_members:
print(f'{member.name}(age={member.age})')
خروجی قطعه کد بالا به شرح زیر است:
John(age=25)
Alice(age=30)
Bob(age=35)