سطح مقدماتی (Beginner Level)
در پایتون، کلاسها و اشیاء به صورت پیشفرض بسیار پویا (Dynamic) هستند. این بدان معناست که شما میتوانید در هر زمانی ویژگیهای (Attributes) جدیدی را به یک شیء اضافه کنید. پایتون برای مدیریت این پویایی، ویژگیهای هر شیء را در یک دیکشنری مخفی به نام __dict__ ذخیره میکند.
اگرچه این مکانیزم انعطافپذیری بالایی دارد، اما دیکشنریها ساختارهایی هستند که حافظه (RAM) زیادی مصرف میکنند. زمانی که شما هزاران یا میلیونها نمونه از یک کلاس میسازید، این سربار حافظه میتواند مشکلساز شود. مکانیزم __slots__ راه حلی است که پایتون برای این مشکل ارائه داده است.
۱. مفهوم __slots__ چیست؟
با تعریف __slots__ در یک کلاس، شما به پایتون میگویید: «این کلاس فقط و فقط این ویژگیهای مشخص را دارد و قرار نیست ویژگی دیگری به آن اضافه شود». در نتیجه، پایتون به جای ایجاد یک دیکشنری (__dict__) برای هر شیء، فضای حافظه ثابتی را فقط برای همان ویژگیها تخصیص میدهد.
مزایای اصلی:
- کاهش مصرف حافظه: حذف دیکشنری به ازای هر شیء.
- دسترسی سریعتر: دسترسی به ویژگیها کمی سریعتر میشود.
- جلوگیری از خطای تایپی: امکان اضافه کردن ویژگیهای ناخواسته وجود ندارد.
مثال اول: ساختار بدون اسلات (معمولی)
در کلاسهای معمولی، ویژگیها در __dict__ ذخیره میشوند.
مثال دوم: استفاده از __slots__
در اینجا ما لیستی از نام ویژگیها را به __slots__ میدهیم. پایتون دیگر __dict__ را ایجاد نمیکند.
# تعریف کلاس با __slots__ (Static Snippet)
class SlottedPoint:
# به پایتون میگوییم فقط x و y را میشناسیم
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
مثال سوم: رفتار سختگیرانه اسلاتها
اگر سعی کنید ویژگیای که در __slots__ تعریف نشده است را به شیء اختصاص دهید، با خطای AttributeError مواجه میشوید.
سطح پیشرفته (Professional Level)
در سطح حرفهای، استفاده از __slots__ فراتر از صرفهجویی ساده در حافظه است. نکاتی در مورد ارثبری (Inheritance)، ترکیب با دیکشنریها، و تاثیر بر ابزارهای اشکالزدایی وجود دارد که باید بدانید.
۱. مقایسه دقیق مصرف حافظه
تفاوت حافظه در برنامههای مقیاسبزرگ (Large-Scale) بسیار چشمگیر است. دیکشنریها در پایتون ساختارهای Hash Map هستند و برای جلوگیری از برخورد (Collision) فضای خالی زیادی نگه میدارند. __slots__ شبیه به یک آرایه (Array) یا ساختار C-struct عمل میکند.
بررسی تکنیکی سایز اشیاء
در مثال زیر با استفاده از کتابخانه pympler (یا منطق مشابه با sys) تفاوت را نشان میدهیم. توجه داشته باشید که sys.getsizeof به تنهایی ممکن است سایز اشیاء ارجاع شده را نشان ندهد، اما سایز خودِ شیء کانتینر را به خوبی نشان میدهد.
۲. ارثبری و چالشهای آن (Inheritance Caveats)
رفتار __slots__ در ارثبری میتواند گیجکننده باشد.
- فرزند بدون اسلات: اگر کلاس والد
__slots__داشته باشد اما کلاس فرزند نداشته باشد، کلاس فرزند به طور خودکار__dict__خواهد داشت و مزیت حافظه تا حد زیادی از بین میرود. - افزودن اسلات در فرزند: اگر فرزند هم
__slots__تعریف کند، اسلاتهای نهایی ترکیبی از اسلاتهای والد و فرزند خواهند بود.
مثال سناریوی ارثبری
۳. ترکیب __slots__ و __dict__
گاهی اوقات شما میخواهید مزایای سرعت و حافظه را برای ویژگیهای اصلی داشته باشید، اما همچنان اجازه دهید ویژگیهای داینامیک اضافه شوند. برای این کار میتوانید '__dict__' را به تاپلِ اسلاتها اضافه کنید.
# تعریف کلاسی که هم بهینه است و هم پویا
class HybridClass:
# فضای فیکس برای name و id، اما یک دیکشنری برای سایر موارد
__slots__ = ('name', 'id', '__dict__')
def __init__(self, name, id):
self.name = name
self.id = id
تست عملی کلاس ترکیبی
۴. نکات نهایی و فنی
- Weak References: اگر میخواهید اشیاء کلاس شما قابلیت ارجاع ضعیف (Weak Reference) داشته باشند، باید
'__weakref__'را نیز به لیست اسلاتها اضافه کنید، در غیر این صورت پشتیبانی نمیشود. - سرعت دسترسی: دسترسی به ویژگیهای اسلاتدار کمی سریعتر است زیرا پایتون از ایندکسگذاری آرایه استفاده میکند تا جستجوی Hash در دیکشنری، اما این تفاوت سرعت معمولاً دلیل اصلی استفاده از آن نیست (دلیل اصلی حافظه است).