GreatBahram
GreatBahram
خواندن ۹ دقیقه·۵ سال پیش

یک‌بار برای همیشه - Descriptor

Oh! Descriptors, after all!
Oh! Descriptors, after all!

سلام!

توی این قسمت از یکبار برای همیشه هدف‌مون معرفی Descriptor هست. در یک‌بار برای همیشه بحثی رو مطرح می‌کنیم که خیلی در نگاه اول شاید راحت بنظر نرسه، و سعی می‌کنیم با بیان ساده‌تر و مثال‌های خیلی خیلی ابتدایی اون مطلب رو به راحتی به مخاطب انتقال بدیم.

از اون‌جایی که Descriptor جز مفاهیمی هست که شاید توسعه‌دهنده‌های پایتون، به صورت خود‌جوش به سمتش نرن و همچنین عدم وجود مطلب در این زمینه از جمله دلایلی باشه که این مفهوم سخت بنظر برسه، تصمیم گرفتم که این مطلب رو بنویسم.

علاوه بر این، descriptorها چون در موارد خاصی به کار گرفته می‌شوند، طبیعی هست که بعد از یک مدت دانش ما در این رابطه کم‌رنگ‌تر بشه.

  • تمام‌ کدهایی استفاده شده، اینجا قابل دسترس هستند.

مقدمه

در قسمت قبل راجبعه property صحبت کردیم (اگر این قسمت رو نخوندید حتما پیشنهاد میشه قبل از خوندن این مطلب، بخش قبلی رو بخونید) و گفتیم معادل Setter/Getter در پایتون هست و هم باعث خوانایی کدمون میشد و می‌تونستیم از lazy loadingاش بهره ببریم.

به این نتیجه هم رسیدیم که property ها جزئی از یک مفهوم بزرگتری به نام descriptor هستند، که بستری رو برای مدیریت دسترسی به attributeهای یک آبجکت رو فراهم می‌کنند. و همچنین گفته شد که اساساً attributeها سه عمل رو پشتیبانی می‌کنند:

  • دسترسی به مقدار attribute
access to an object's attribute
access to an object's attribute
  • تغییر مقدار attribute
modify an object's attribute
modify an object's attribute
  • حذف مقدار attribute
delete an object's attribute
delete an object's attribute

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

فرض کردیم اگر یه descriptor به NonNegative داشتیم چقدر کارمون راحت‌تر می‌شد. توی قطعه کد پایین ما یکبار یک Descriptor تعریف کردیم که به محض دریافت ورودی چک می‌کنه اگر مقدار ورودی منفی باشه خطا میده؛ اگر نه ورودی رو توی آبجکت ما ذخیره می‌کنه. این کد با اون روش که یک منطق رو هی تکرار می‌کردیم واقعا قابل مقایسه نیست.

Movie object
Movie object

حالا توی این قسمت، اول از همه خود Descriptor رو تعریف می‌کنیم بعدش قدم به قدم، متد‌های لازم رو پیاده‌سازی می‌کنیم و نهایتا دو مثال پیاده‌سازی شده از descriptor با هم می‌بینیم.

به چه چیزی descriptor گفته میشه؟

احتمالاً شما خیلی از جاها Descriptorها رو دیدید، اما شاید در اون برهه از زمان، خودِ descriptor برای شما اهمیت نداشته. مثلاً توی اکثر فریم‌ورک‌های وب از descriptor برای ساخت مدل در دیتابیس به صورت زیر استفاده میشه:

Flask-SQLAlchmey
Flask-SQLAlchmey
یک نکته‌ای که خوب هست بهش توجه بشه این هست که تمام این متغییرها بیرون از متدِ __init__ نوشته شدند و در نتیجه همه‌ی اونا Class variable محسوب می‌شن.

اما بریم ببینیم descriptor چی هست؟

به زبون خیلی ساده descriptor آبجکتی هست که یک سری رفتار رو به attribute های مورد نظر bind (وصل) می‌کنه.

از نظر فنی، هر کلاسی که یکی از متدهایی زیر رو داشته باشه بهش descriptor می‌گیم:

  • متد __get__، که وقتی قرار هست به مقدار attribute دسترسی داشته باشیم اجرا میشه.
  • متد __set__، که هنگام set کردن مقدار به attribute اجرا میشه.
  • و نهایتا __delete__، که هر موقع قصد حذف یک attribute رو داریم اجرا میشه.

هر کدوم از این متد‌ها یه ورودی‌های ثابتی می‌گیرن که چون اساسا descriptor یک پروتکل هست و خود پایتون معمولا با این متد‌ها تعامل داره، نه کاربر!

یادآوری
برای اینکه ببینیم هر آبجکت توی پایتون چه attribute داره از متد ‍`__dict__` کمک می‌گیریم. یه راه دیگه هم که خوناتر هست اینه که از تابع vars استفاده کنیم.

Get a list of an object or class attributes
Get a list of an object or class attributes

همین تابع رو روی خود کلاس اگر صدا بزنیم، خروجی‌اش این میشه:

اولین متد، متد get

همون‌طور که گفتیم متد __get__ برای دسترسی به مقدار یک attribute اجرا میشه. این متد علاوه بر خود آبجکت (self) دو تا ورودی دیگه هم داره.

Descriptor get method
Descriptor get method

توی مثال بالا، ما متد __get__ رو تعریف کردیم، متد get دو تا ورودی داره: instance و obj_type.

توی مثال بالا m یک instance از کلاس Movie هست و خود کلاس Movie نقش obj_type رو داره.

  • ورودی obj_type معمولا خالی هست، مگر موقعی که attribute از طریق کلاس صدا زده بشه مثل پایین (دقت کنید از Movie هیچ instance ای ساخته نشده):

توی این حالت، instance مقدار None رو داره. چون دیگه با سروکار ما با instance نیست. ما در حقیقت از خود کلاس آگاه هستیم تا بتونیم کاری کنیم که descriptor ما صدا زدن از طریق class - بدون ساختن instance - رو داشته باشه. عملا، کاری که پایتون تو لایه‌های زیرین داره انجام میده این جوری هست:

حالت دوم، وقتی هم هست که از طریق یکی از instanceهای Movie به price می‌خواهیم دسترسی داشته باشیم. مثل:

که پایتون این‌جوری در حقیقت داره descriptor ما رو فراخوانی می‌کنه:

پس توی این حالت ما هم به instance و هم به type آبجکت دسترسی داریم. اگر توضیحات یکم گیج‌تون کرده ادامه بدید جلوتر شفاف تر میشه.

متد set و delete

این متد (__set__) همون‌طور که پیداست برای ذخیره/ویرایش attribute هست.

Descriptor set and delete methods
Descriptor set and delete methods

همون‌طور که می‌بینید دیگه توی این دو متد خبری از obj_type نیست و تمام کار ما با instance انجام میشه. اگر هم کنجکاوید که پایتون چجوری داره یک لایه پایین این کار رو انجام میده، مشابه پایین رو انجام میده:

و نهایتاً با del کردن یک attribute هم، کارهای زیر انجام میشه:

می‌سازمت Descriptor

حالا که یکم با خود مفهومش آشنا شدیم بریم سراغ پیاد‌‌ه‌سازی تا به درک خوبی برسیم.

مثال اول

بریم مشکلی که با property حل نشد رو با descriptor حل کنیم. می‌خواهیم یک descriptor بسازیم به اسم NonNegative که هم بشه ازش داده رو گرفت و هم ذخیره کرد.

خب ببینیم توی کد بالا ما چه کارهایی کردیم:

  • بازم تاکید می‌کنم این Descriptor وقتی به صورت class variable استفاده می‌شه، درست عمل می‌کنه. چون پایتون می‌دونه باید بره سراغ متد های مخصوص Descriptor.
  • دومین نکته، اینه که ما داریم تمام مقادیر رو توی instanceهایی از NonNegative ذخیره می‌کنیم (یعنی مقادیر داخل instanceهای Movie نیستن بلکه یه نگاشتی اون وسط اتفاق افتاده). راجعبه این کار حرف و حدیث‌های زیادی هست. در حقیقت برای ذخیره سازی‌ مقادیر دو راه اصلی وجود داره: اول اینکه مقادیر رو توی خود Descriptor ذخیره می‌کنید(مثل همین حالت) یا داخل instanceهای کلاس مورد نظر (مثلاً Movie) که روش دوم (بنظر) منطقی‌تر میرسه. ولی اگر اینا رو ببریم داخل instance ورودی با چه اسمی اونجا ذخیره‌شون کنیم. می‌خوام بگم ما اینجا به اسم متعییر (توی مثال بالا rating) دسترسی نداریم. یکی ممکن هست بگه که کاری نداره می‌تونیم از کاربر بخواهیم موقع استفاده اسم هر متغییر رو هم مشخص کنه و اون موقع به راحتی می‌تونیم داخل خود instance ها ذخیره‌اش کنیم:
Getting names at the initialization time
Getting names at the initialization time

اشکال این کار مشخص هست دیگه؟ یکم غیر منطقی به نظر می‌رسه. اما چی‌میشد اگر این کارو خود پایتون یه جوری مدیریت‌اش می‌کرد؟ خبر خوب اینه که توی پایتون ۳.۶ یه متد دیگه به پروتکل Descriptorها اضافه شده که این امکان رو برای ما فراهم می‌کنه. نحوه استفاده ازشم به صورت پایین هست:

Better way in Python 3.6+
Better way in Python 3.6+

همون‌طور که می‌بینیم این متد جدید (__set_name__) کار ما رو خیلی راحت کرد (البته اینم بگم که به این صورت نبوده که هیچ‌راه‌حلی قبلش نباشه، توی نسخه ۳.۶ این‌کار راحت‌تر شده). حالا چون اسم متغییر‌ها رو از ورودی گرفتم،‌می‌تونیم به راحتی مقادیر رو داخل لیست ویژگی‌های instance ورودی ذخیره کنیم. حالا اگر یکبار دیگه به اون مثال دیتابیس نگاه کنید می‌بینید اونجا هم عملا هیچ‌کجا بهش اسم متغییر رو ندادیم(توی روش‌های قدیمی تر با کمک metaclass این کارو انجام می‌دادن).

یکم بهتر

ممکن هست شما validator های مختلفی نیاز داشته باشید،‌ می‌تونیم کد بالا رو یکم بهتر‌ش کنیم که بتونیم ازش به راحتی بتونیم descriptor های دیگه بسازیم. اول از همه یک Base Class می‌سازیم:

Let's the fun be gin!
Let's the fun be gin!

و حالا می‌تونیم برای هر کار دیگه از این Base classمون استفاده کنیم:

انواع Descriptor

یه دسته‌بندی هم وجود داره که میگه اگر یک Descriptor فقط متد get رو تعریف کرده باشه بهش non-data descriptor می‌گیم. مثلا این دو تا دوست خوب‌مون staticmethod و classmethod دوتاشون non-data محسوب می‌شن. از اون طرف به descriptor که متد‌های دیگه رو علاوه بر get داشته باشه data descriptor گفته میشه.

اما اینکه چرا اینا رو از هم دیگه متمایز کردن، به خاطر نحوه attribute lookup کردن داخل پایتون هست.

یک متد خاصی هست به اسم getattribute که هر بار با attribute های یک ابجکت کار می‌کنیم، یک روال مشخصی رو طی می‌کنه که بتونه اون خصیصه مورد نظر ما رو پیدا کنه (دسترسی به attribute هم مشخص هست دیگه مثلا اینجوری instance.name). این متد سه گام اساسی داره:

  1. اول این مشخص می‌کنه این attribute یک data descriptor هست یا نه.
  2. اگر نباشه میره داخل خصیصه‌های خود آبجکت (__dict__) دنبال اون خصیصه می‌گرده.
  3. توی مرحله سوم،‌اگر پیداش نکرد بررسی می‌کنه ببینه که یک non-data descriptor هست، اگر آره اون رو اجرا می‌کنه.

این به چه معنی هست؟ که چی اینا رو گفتی؟ یعنی اگر ما یک non-data descriptor بسازیم،‌ دفعه اول که اجرا می‌شیم (با این فرض که جوری هم پیاده‌سازی‌اش کرده باشیم که حاصل رو داخل خود instance ورودی ذخیره کنیم). سری بعدی که کاربر می‌خواد مقدار attribute رو بگیره. دیگه descriptor ما اجرا نمیشه بلکه توی مرحله دوم بالا می‌مونه، در حالی که اگر data descriptor باشه حتی اگر داخل instance هم مقدار رو ذخیره کنیم هر بار متد get اجرا میشه (توضیح بیشتر این مطلب).

مثال دوم

می‌خواهیم یک descriptor بسازیم که مشابه property باشه با این تفاوت که مقدار محاسبه شده رو هم کش کنه،‌ اسم‌اش رو هم می‌سازیم cached_property (مشابه این ایده هم تقریبا توی خیلی از پروژه‌ها از جنگو تا خود pip وجود داره).

با دانشی که الان داریم می‌دونیم که:

  • با کمک set_name به راحتی می‌تونیم اسم متغییر رو بدست بیاریم.
  • به راحتی می‌تونیم مقدار مورد نظرمون رو داخل instance ورودی ذخیره کنیم.
  • برای اینکه کار هم سخت نشه به صورت non-data پیاده‌سازی‌اش می‌کنیم.
Simplified version of cached_property
Simplified version of cached_property

استفاده از اون، این اینجوری میشه (دقت کنید که چون non-data descriptor هست، عبارت computing فقط یکبار چاپ شده)

Use descriptors as decorator
Use descriptors as decorator
  • یکی ممکن هست بپرسه چرا ما تونستیم به صورت decorator ازش استفاده کنیم؟ چون تمام Descriptorها به صورت پیش‌فرض امکان استفاده به صورت Decorator رو هم دارند، این چیزی هست که به خودی خود وجود داره و نیازی به کاری از سمت شما نداره. بهمین خاطر هست که ما property، classmethod و staticmethod رو به صورت decorator استفاده می‌کنیم.
  • حالا میشه به عنوان تمرین این رو تبدیل به data descriptor کرد و با هم مقایسه‌شون کرد (اون قضیه attribute lookup رو هم بررسی کنید) و حالا که تبدیل به data descriptorش کردید، کاری کنید که بشه مقدار جدید هم بهش داد.
  • البته این رو هم بگم descriptor بالا به صورت non-data داخل کتابخانه‌های پایتون هم وجود داره.

جمع‌بندی

همون‌طور که گفتیم property خیلی چیز باحالیه و خیلی از جاها کار ما رو راه می‌اندازه. اما اگر جایی دیدیم که داریم خودمون رو هی تکرار می‌کنیم؛ نشون از وجود راه‌ بهتری هست. و یکی از را‌ه‌ها کمک گرفتن از descriptor هست. حتی اگر از این مفهوم استفاده نکنید؛ امیدوارم که درک‌تون از رفتار آبجکت‌های داخلی پایتون بالاتر رفته باشه.

امید‌وارم که از این مطلب چیزی یاد گرفته باشید.

از اینجا کجا بریم

  • تمام‌ کدهایی که استفاده شده، اینجا قابل دسترس هستند.
  • اگر علاقمند هستید که بیشتر بدونید، کد‌ پیاده‌سازی property نقطه شروع خیلی خوبی می‌تونه باشه. اگر می‌تونید قبل از خوندن کد، سعی کنید خودتون property رو بسازید.
  • اینجا هم یکی از مراجع اصلی هست که من هم ازش یاد گرفتم و از اطلاعات‌اش اینجا استفاده کردیم.

https://nbviewer.jupyter.org/urls/gist.github.com/ChrisBeaumont/5758381/raw/descriptor_writeup.ipynb

  • معرفی جامع‌تر این موضوع از RealPython

https://realpython.com/python-descriptors/

pythonproperty
Pythonista, Free Software Enthusiast. GNU/Linux Master. Network Security Researcher. Son. Brother.
شاید از این پست‌ها خوشتان بیاید