بررسی خصوصیت __slots__ در پایتون — از صفر تا صد

۱۳۲ بازدید
آخرین به‌روزرسانی: ۰۴ مهر ۱۴۰۲
زمان مطالعه: ۵ دقیقه
بررسی خصوصیت __slots__ در پایتون — از صفر تا صد

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

1class WithSlots:
2    __slots__ = ('x', 'y')
3
4    def __init__(self, x, y):
5        self.x, self.y = x, y

برای وهله‌های این کلاس می‌توانیم از self.x و self.y به همان روش وهله‌های کلاس معمولی استفاده کنیم. با این حال، یکی از تفاوت‌های کلیدی بین این روش و وهله‌سازی از کلاس نرمال این است که نمی‌توانید خصوصیت‌ها را از این وهله کلاس حذف یا اضافه کنید. فرض کنید یک وهله از کلاس w نام دارد. به این ترتیب دیگر نمی‌توانیم کدی مانند w.z=2 بنویسیم، چون موجب ایجاد خطا می‌شود.

بزرگ‌ترین دلیل سطح بالای استفاده از __slots__ ابتدا دریافت و تعیین سریع‌تر خصوصیت به دلیل بهینه‌سازی ساحتمان داده آن و سپس کاهش مصرف حافظه در وهله‌های کلاس است. برخی دلایلی که ممکن است نخواهید از اسلات‌ها استفاده کنید این است که خصوصیت‌های کلاس در زمان اجرا تغییر یابند و یا این که درخت وراثت شیء پیچیده باشد.

تست کردن

ابتدا برخی تست‌ها را انجام می‌دهیم تا ببینیم آیا واقعاً __slots__ سریع‌تر است یا نه. کار خود را از وهله‌سازی انبوه آغاز می‌کنیم.

از ماژول temeit پایتون و این قطعه کد استفاده می‌کنیم تا نتیجه زیر حاصل شود:

1class WithoutSlots:
2    def __init__(self, x, y, z):
3        self.x = x
4        self.y = y
5        self.z = z
6
7class WithSlots:
8    __slots__ = ('x', 'y', 'z')
9
10    def __init__(self, x, y, z):
11        self.x = x
12        self.y = y
13        self.z = z
14
15def instance_fn(cls):
16    def instance():
17        x = cls(1, 2, 3)
18    return instance

نتیجه به صورت زیر است:

Without Slots: 0.3909880230203271
With Slots: 0.31494391383603215
(averaged over 100000 iterations)

چنان که می‌بینید وهله‌سازی با اسلات‌ها در این مورد کمی سریع‌تر بوده است. این نتیجه معنی‌داری است، ‌زیرا ایجاد __dict__ برای وهله‌های جدید از شیء مفروض انجام نمی‌گیرد. دیکشنری‌ها به طور کلی سربار بیشتری نسبت به چندتایی‌ها و لیست‌ها دارند. این موضوع را با یک کلاس که خصوصیت‌های بسیار بیشتری در ارتباط با یک وهله دارد بررسی می‌کنیم. این مثال 26 خصوصیت دارد:

Without Slots: 1.5249411426484585
With Slots: 1.52750033326447
(averaged over 100000 iterations)

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

افزایش واقعی سرعت در زمان ایجاد و تعیین مقادیر با توالی سریع رخ می‌دهد:

1def get_set_fn(cls):
2    x = cls(list(range(26)))
3    def get_set():
4        x.y = x.z + 1
5        x.a = x.b - 1
6        x.d = x.q + 3
7        x.i = x.j - 1
8        x.z = x.y / 2
9    return get_set

نتیجه به صورت زیر است:

Without Slots: 11.59717286285013
With Slots: 9.243316248897463
(averaged over 100000 iterations)

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

مصرف حافظه

ابتدا به بررسی تفاوت رشد چندتایی‌ها و دیکشنری‌ها در حافظه می‌پردازیم. زمانی که از __slots__ استفاده می‌کنیم، می‌دانیم که کدام خصوصیت‌ها می‌توانند برای یک وهله خاص وجود داشته باشند و می‌تواند برای توصیف‌های مرتبط با یک وهله تخصیص یابد. نمایش مقدار دقیق حافظه مصرف شده از سوی یک وهله از شیء در پایتون کار دشواری است. sys.getsizeof تنها برای انواع primitive و داخلی پایتون کار می‌کند. به جای آن از یک تابع به نام asizeof در کتابخانه‌ای به نام Pympler استفاده می‌کنیم.

1>>> asizeof(('a', 'b', 'c', 'd'))
2304
3>>> asizeof({'a': 'b', 'c': 'd'})
4512
5>>> asizeof(tuple(string.ascii_lowercase))
61712
7>>> dictionary
8{'e': 'f', 'k': 'l', 'c': 'd', 'g': 'h', 'o': 'p', 'i': 'j', 's': 't', 'm': 'n', 'q': 'r', 'a': 'b', 'y': 'z', 'w': 'x', 'u': 'v'}
9>>> asizeof(dictionary)
102320

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

1>>> asizeof(('a', 'b')) + asizeof(('c', 'd'))
2352

همچنین زمانی که asizeof را عملاً روی مثال قبلی از یک کلاس اسلات شده اجرا کنیم، نتیجه زیر حاصل می‌شود:

1>>> w1 = WithoutSlots(1, 2, 3)
2>>> asizeof(w1)
3416
4>>> w2 = WithSlots(4, 5, 6)
5>>> asizeof(w2)
6160

جزییات پیاده‌سازی CPython

ابتدا باید برخی موارد را در مورد ماهیت CPython روشن کنیم. CPython یک پیاده‌سازی استاندارد از زبان پایتون و هسته آن است که با استفاده از زبان C نوشته شده است. زمانی که python3 را روی سیستم نصب و اجرا می‌کنید احتمالاً از این نسخه استفاده می‌کنید. سورس آن را می‌توانید از این صفحه (+) دانلود کنید.

در این بخش به بررسی تفاوت‌های رخ داده در یک کلاس در زمان تعریف کردن با __slots__ می‌پردازیم و همچنین برخی خصوصیات نسخه 3.7.1 CPython’s را نیز بررسی می‌کنیم.

  • زمانی که __slots__ در یک کلاس وهله‌سازی‌شده ظاهر شوند، __dict__ برای وهله جدید ایجاد نمی‌شود. اما در صورتی که __dict__ را به __slots__ اضافه کنید، دیکشنری نیز وهله‌سازی می‌شود، یعنی می‌توانید از مزیت هر دوی آن‌ها بهره‌مند شوید. فایل‌ها: typeobject.c type_new.
  • وهله‌سازی برای کلاس‌های دارای __slots__ به کار بیشتری نسبت به ایجاد __dict__ نیاز دارد. چون روی همه مقادیر تعریف‌شده در مدخل دیکشنری __slots__ مربوط به کلاس تعریف می‌شوند و برای هر مدخل باید توصیفی درج شود. به منظور کسب اطلاعات بیشتر type_new را در typeobject.c بررسی کنید.
  • بایت‌کد تولید شده برای کلاس‌های دارای اسلات و بدون آن برابر هستند. این بدان معنی است که تفاوت‌های lookup به شیوه اجرای LOAD_ATTR در opcode مربوط است. به منظور کسب اطلاعات بیشتر dis.dis را که یک دی‌اسمبلر بایت‌کد پایتون است بررسی کنید.
  • چنان که می‌توان انتظار داشت، فقدان __slots__ منجر به lookup دیکشنری می‌شود. اگر به جزییات این موضوعات علاقه‌مند هستید، PyDict_GetItem را بررسی کنید. در این وضعیت، در نهایت یک اشاره‌گر به PyObject به دست می‌آوریم که مقدار مورد جستجو را در دیکشنری به دست می‌دهد. با این حال، اگر __slots__ را دارید، توصیفگر کش می‌شود. در PyMember_GetOne از آفست توصیفگر برای پرش مستقیم به مکان ذخیره اشاره‌گر در حافظه استفاده می‌شود. این امر موجب بهبود اندکی در انسجام کش می‌شود، چون اشاره‌گر‌های اشیا در دسته‌های 8 بایتی در کنار همدیگر قرار می‌گیرند. با این حال، همچنان یک اشاره‌گر PyMember_GetOne است، یعنی باید جایی در حافظه ذخیره شود.

برخی اشاره‌گر‌های GDB

اگر می‌خواهید به بررسی عمیق‌تر CPython بپردازید، پیش از آغاز کدنویسی و اجرای تابع‌ها باید برخی مراحل آماده‌سازی را طی کنید. پس از دانلود کردن سورس و نصب پکیج‌های مورد نیاز، به جای اجرای بی‌درنگ دستور configure/.، دستور configure --with-pydebug/ را اجرا کنید. این دستور یک بیلد دیباگ از پایتون به جای بیلد نرمال می‌سازد. به این ترتیب می‌توانید GDB را به پردازش الصاق کنید. سپس باید make را اجرا کنیم تا یک فایل باینری ایجاد کرده و آن را با استفاده از GDB با اجرای دستور gdb python دیباگ کنیم.

همچنین در صورتی که بخواهید کد واقعی پایتون را دیباگ کنید، دو راهبرد وجود دارد. یا باید یک نقطه توقف شرطی ایجاد کنید که با استفاده از رشته type->tp_name در GDB متوقف شود و یا عملاً یک گزاره if بنویسید و یک نقطه توقف را درون این گزاره قرار دهید. ما از راهبرد دوم استفاده کردیم، ‌زیرا هر بار چسباندن یک گزاره بلند نقطه توقف شرطی در GDB کاری آزاردهنده است.

سخن پایانی

در این مقاله ابتدا به بررسی استفاده از __slots__ در پایتون پرداختیم. سپس پایتون را دانلود کرده و آن را بیلد کردیم و گزاره‌های printif را در مکان‌های تصادفی آن قرار داده و از gdb استفاده کردیم. به این ترتیب به بررسی دلایل ارائه شده برای استفاده از __slots__ پرداختیم. در این مسیر موارد جدید زیادی را آموختیم.

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
usejournal
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *