سلام!
توی این قسمت از یکبار برای همیشه هدفمون معرفی Descriptor هست. در یکبار برای همیشه بحثی رو مطرح میکنیم که خیلی در نگاه اول شاید راحت بنظر نرسه، و سعی میکنیم با بیان سادهتر و مثالهای خیلی خیلی ابتدایی اون مطلب رو به راحتی به مخاطب انتقال بدیم.
از اونجایی که Descriptor جز مفاهیمی هست که شاید توسعهدهندههای پایتون، به صورت خودجوش به سمتش نرن و همچنین عدم وجود مطلب در این زمینه از جمله دلایلی باشه که این مفهوم سخت بنظر برسه، تصمیم گرفتم که این مطلب رو بنویسم.
علاوه بر این، descriptorها چون در موارد خاصی به کار گرفته میشوند، طبیعی هست که بعد از یک مدت دانش ما در این رابطه کمرنگتر بشه.
در قسمت قبل راجبعه property صحبت کردیم (اگر این قسمت رو نخوندید حتما پیشنهاد میشه قبل از خوندن این مطلب، بخش قبلی رو بخونید) و گفتیم معادل Setter/Getter در پایتون هست و هم باعث خوانایی کدمون میشد و میتونستیم از lazy loadingاش بهره ببریم.
به این نتیجه هم رسیدیم که property ها جزئی از یک مفهوم بزرگتری به نام descriptor هستند، که بستری رو برای مدیریت دسترسی به attributeهای یک آبجکت رو فراهم میکنند. و همچنین گفته شد که اساساً attributeها سه عمل رو پشتیبانی میکنند:
توی مثال قسمت قبل، که هدفمون مدیریت مقادیر منفی بود، با کمک property این عمل رو مدیریت کردیم. اما چون تعدادشون زیاد بود، به این فکر افتادیم که شاید property بهترین راهحل نباشه و راجعبه یک راهحل بهتر هم یکم صحبت کردیم: Descrtiptor.
فرض کردیم اگر یه descriptor به NonNegative داشتیم چقدر کارمون راحتتر میشد. توی قطعه کد پایین ما یکبار یک Descriptor تعریف کردیم که به محض دریافت ورودی چک میکنه اگر مقدار ورودی منفی باشه خطا میده؛ اگر نه ورودی رو توی آبجکت ما ذخیره میکنه. این کد با اون روش که یک منطق رو هی تکرار میکردیم واقعا قابل مقایسه نیست.
حالا توی این قسمت، اول از همه خود Descriptor رو تعریف میکنیم بعدش قدم به قدم، متدهای لازم رو پیادهسازی میکنیم و نهایتا دو مثال پیادهسازی شده از descriptor با هم میبینیم.
احتمالاً شما خیلی از جاها Descriptorها رو دیدید، اما شاید در اون برهه از زمان، خودِ descriptor برای شما اهمیت نداشته. مثلاً توی اکثر فریمورکهای وب از descriptor برای ساخت مدل در دیتابیس به صورت زیر استفاده میشه:
یک نکتهای که خوب هست بهش توجه بشه این هست که تمام این متغییرها بیرون از متدِ __init__ نوشته شدند و در نتیجه همهی اونا Class variable محسوب میشن.
به زبون خیلی ساده descriptor آبجکتی هست که یک سری رفتار رو به attribute های مورد نظر bind (وصل) میکنه.
از نظر فنی، هر کلاسی که یکی از متدهایی زیر رو داشته باشه بهش descriptor میگیم:
هر کدوم از این متدها یه ورودیهای ثابتی میگیرن که چون اساسا descriptor یک پروتکل هست و خود پایتون معمولا با این متدها تعامل داره، نه کاربر!
یادآوری
برای اینکه ببینیم هر آبجکت توی پایتون چه attribute داره از متد `__dict__` کمک میگیریم. یه راه دیگه هم که خوناتر هست اینه که از تابع vars استفاده کنیم.
همین تابع رو روی خود کلاس اگر صدا بزنیم، خروجیاش این میشه:
همونطور که گفتیم متد __get__ برای دسترسی به مقدار یک attribute اجرا میشه. این متد علاوه بر خود آبجکت (self) دو تا ورودی دیگه هم داره.
توی مثال بالا، ما متد __get__ رو تعریف کردیم، متد get دو تا ورودی داره: instance و obj_type.
توی مثال بالا m یک instance از کلاس Movie هست و خود کلاس Movie نقش obj_type رو داره.
توی این حالت، instance مقدار None رو داره. چون دیگه با سروکار ما با instance نیست. ما در حقیقت از خود کلاس آگاه هستیم تا بتونیم کاری کنیم که descriptor ما صدا زدن از طریق class - بدون ساختن instance - رو داشته باشه. عملا، کاری که پایتون تو لایههای زیرین داره انجام میده این جوری هست:
حالت دوم، وقتی هم هست که از طریق یکی از instanceهای Movie به price میخواهیم دسترسی داشته باشیم. مثل:
که پایتون اینجوری در حقیقت داره descriptor ما رو فراخوانی میکنه:
پس توی این حالت ما هم به instance و هم به type آبجکت دسترسی داریم. اگر توضیحات یکم گیجتون کرده ادامه بدید جلوتر شفاف تر میشه.
این متد (__set__) همونطور که پیداست برای ذخیره/ویرایش attribute هست.
همونطور که میبینید دیگه توی این دو متد خبری از obj_type نیست و تمام کار ما با instance انجام میشه. اگر هم کنجکاوید که پایتون چجوری داره یک لایه پایین این کار رو انجام میده، مشابه پایین رو انجام میده:
و نهایتاً با del کردن یک attribute هم، کارهای زیر انجام میشه:
حالا که یکم با خود مفهومش آشنا شدیم بریم سراغ پیادهسازی تا به درک خوبی برسیم.
بریم مشکلی که با property حل نشد رو با descriptor حل کنیم. میخواهیم یک descriptor بسازیم به اسم NonNegative که هم بشه ازش داده رو گرفت و هم ذخیره کرد.
خب ببینیم توی کد بالا ما چه کارهایی کردیم:
اشکال این کار مشخص هست دیگه؟ یکم غیر منطقی به نظر میرسه. اما چیمیشد اگر این کارو خود پایتون یه جوری مدیریتاش میکرد؟ خبر خوب اینه که توی پایتون ۳.۶ یه متد دیگه به پروتکل Descriptorها اضافه شده که این امکان رو برای ما فراهم میکنه. نحوه استفاده ازشم به صورت پایین هست:
همونطور که میبینیم این متد جدید (__set_name__) کار ما رو خیلی راحت کرد (البته اینم بگم که به این صورت نبوده که هیچراهحلی قبلش نباشه، توی نسخه ۳.۶ اینکار راحتتر شده). حالا چون اسم متغییرها رو از ورودی گرفتم،میتونیم به راحتی مقادیر رو داخل لیست ویژگیهای instance ورودی ذخیره کنیم. حالا اگر یکبار دیگه به اون مثال دیتابیس نگاه کنید میبینید اونجا هم عملا هیچکجا بهش اسم متغییر رو ندادیم(توی روشهای قدیمی تر با کمک metaclass این کارو انجام میدادن).
ممکن هست شما validator های مختلفی نیاز داشته باشید، میتونیم کد بالا رو یکم بهترش کنیم که بتونیم ازش به راحتی بتونیم descriptor های دیگه بسازیم. اول از همه یک Base Class میسازیم:
و حالا میتونیم برای هر کار دیگه از این Base classمون استفاده کنیم:
یه دستهبندی هم وجود داره که میگه اگر یک Descriptor فقط متد get رو تعریف کرده باشه بهش non-data descriptor میگیم. مثلا این دو تا دوست خوبمون staticmethod و classmethod دوتاشون non-data محسوب میشن. از اون طرف به descriptor که متدهای دیگه رو علاوه بر get داشته باشه data descriptor گفته میشه.
اما اینکه چرا اینا رو از هم دیگه متمایز کردن، به خاطر نحوه attribute lookup کردن داخل پایتون هست.
یک متد خاصی هست به اسم getattribute که هر بار با attribute های یک ابجکت کار میکنیم، یک روال مشخصی رو طی میکنه که بتونه اون خصیصه مورد نظر ما رو پیدا کنه (دسترسی به attribute هم مشخص هست دیگه مثلا اینجوری instance.name). این متد سه گام اساسی داره:
این به چه معنی هست؟ که چی اینا رو گفتی؟ یعنی اگر ما یک non-data descriptor بسازیم، دفعه اول که اجرا میشیم (با این فرض که جوری هم پیادهسازیاش کرده باشیم که حاصل رو داخل خود instance ورودی ذخیره کنیم). سری بعدی که کاربر میخواد مقدار attribute رو بگیره. دیگه descriptor ما اجرا نمیشه بلکه توی مرحله دوم بالا میمونه، در حالی که اگر data descriptor باشه حتی اگر داخل instance هم مقدار رو ذخیره کنیم هر بار متد get اجرا میشه (توضیح بیشتر این مطلب).
میخواهیم یک descriptor بسازیم که مشابه property باشه با این تفاوت که مقدار محاسبه شده رو هم کش کنه، اسماش رو هم میسازیم cached_property (مشابه این ایده هم تقریبا توی خیلی از پروژهها از جنگو تا خود pip وجود داره).
با دانشی که الان داریم میدونیم که:
استفاده از اون، این اینجوری میشه (دقت کنید که چون non-data descriptor هست، عبارت computing فقط یکبار چاپ شده)
همونطور که گفتیم property خیلی چیز باحالیه و خیلی از جاها کار ما رو راه میاندازه. اما اگر جایی دیدیم که داریم خودمون رو هی تکرار میکنیم؛ نشون از وجود راه بهتری هست. و یکی از راهها کمک گرفتن از descriptor هست. حتی اگر از این مفهوم استفاده نکنید؛ امیدوارم که درکتون از رفتار آبجکتهای داخلی پایتون بالاتر رفته باشه.
امیدوارم که از این مطلب چیزی یاد گرفته باشید.
https://nbviewer.jupyter.org/urls/gist.github.com/ChrisBeaumont/5758381/raw/descriptor_writeup.ipynb
https://realpython.com/python-descriptors/