ویرگول
ورودثبت نام
DJZ
DJZاولین باری که کامپیوتری رو خراب کردم چون می خواستم ببینم یه قسمت خاصش چه شکلیه فقط یک ساله بودم. ولی مطمئن نیستم آخرین بارم باشه.
DJZ
DJZ
خواندن ۵ دقیقه·۷ روز پیش

Descriptor ها؟! توصیف‌گر ها؟!

به نام خدا

همه چیز از اصل DRY (Don't repeat yourself) شروع میشه و بحث Encapsulation در پایتون. معمولا وقتی که تازه بحث property ها و مثلا setter/getter/deleter رو یاد می گیریم و سعی می کنیم باهاش تمرین کنیم، به یه سری مسئله هایی بر می خوریم که توی اونا یه کلاس چندتا ویژگی از یه نوع (مثلا str) داره. گاهی هم میشه که منطقی که توی setter شون پیاده می کنیم رو توی setter صدتا property دیگه پیاده کردیم. مثال رو ببین:

class Person: def __init__(self, first_name: str, last_name: str, nickname: str): self.__first_name = first_name self.__last_name = last_name self.__nickname = nickname @property def first_name(self) -> str: return self.__first_name @first_name.setter def first_name(self, value: str): if value is None or value == "": raise ValueError('The first name shouldn\'t be empty.') elif not isinstance(value, str): raise TypeError('The first name should be a string.') self.__first_name = value @property def last_name(self) -> str: return self.__last_name @last_name.setter def last_name(self, value: str): if value is None or value == "": raise ValueError('The last name shouldn\'t be empty.') elif not isinstance(value, str): raise TypeError('The last name should be a string.') self.__last_name = value @property def nickname(self) -> str: return self.__nickname @nickname.setter def nickname(self, value: str): if value is None or value == "": raise ValueError('The nickname shouldn\'t be empty.') elif not isinstance(value, str): raise TypeError('The nickname should be a string.') self.__nickname = value

اگه واقعا با دقت و حوصله کل کد رو مطالعه کردی، تبریک میگم جایزه ی فرد با حوصله #1 ویرگول به تو تعلق می گیره. -___-. خودم حتی حوصله نداشتم منطق رو کپی پیست کنم.
می بینی؟ اشکال از کلاس نیست که 3 تا ویژگی با یه اعتبارسنجی داره. مشکل از کدنویسه (که من باشم) که کدشو تکرار می کنه.

شاید بگی چه کاریه، تابع validation تعریف کن و توی هر setter صداش کن. این بدک نیست. جواب میده. اما یه راه حل بهتر هست. اصلا descriptor ها متولد شدن تا به attribute موردنظر ما رفتار دلخواه مون رو بچسبونن. مثل چی؟ مثل همین منطق validation که بررسی می کنیم مقداری که کاربر میده None یا رشته ی خالی نباشه.
قبل این که شروع کنیم: همین الان هم برای این کلاس Person پایتون پشت صحنه خودش شیء descriptor ساخت (ما اگه لازممون بشه باید دستی بنویسیمش، در غیر این صورت پایتون خودش اون رو می سازه.) بریم یکی بسازیم 👇

class NonEmptyString: def __init__(self, name: str): self.name = name

اولین بخشی که توی کلاس descriptor مون تعریف می کنیم مقداردهی اولیه ش هست (چون ما در ادامه از این کلاس چندتا شیء میخوایم.) می پرسی name چی میگه این وسط؟ حق داری، خودم هم وقتی داشتم یاد می گرفتم همین سوال رو پرسیدم. اولا که اگه تازه داری این مبحث رو یاد می گیری، یه کم صبر کن. اگه بریم جلوتر به جواب خیلی از سوال هات می رسی. اما توضیح کوتاهش اینه اسم ویژگی ای هست که میخوایم رفتارهایی رو که در ادامه تعریف می کنیم، بهش بچسبونیم. (مثلا توی مثال کلاس Person، ویژگی های first_name، last_name و nickname)
در ادامه:

def __get__(self, instance, owner):

این امضای این متد هست. instance چیه؟ فرض کن یه جا زده باشی:

p1 = Person('Ali', 'Alavai', 'Ali Agha') print(p1.nickname)

وقتی خط دوم اجرا میشه، دقیقا پشت صحنه توی متد __get__، آرگومان instance تو شده p1 و ownerت شده Person (یا همون type(p1))
👈 ولی وقتی مستقیما بزنی Person.nickname، اون وقت instanceت None هست.

پس بریم بر همین اساس، متد __get__ رو پیاده سازی کنیم:

def __get__(self, instance, owner): if instance is None: print(f'Accessing the {self.name} directly from {owner.__name__}') return self print(f'Accessing the {self.name} from an instance of {owner.__name__}') return instance.__dict__[self.name]

(می تونی منطق دلخواهت رو پیاده کنی. مثلا لاگ گیری پیشرفته تر یا حتی جلوگیری از دسترسی به attribute از کلاس/نمونه) الان نپرس چرا توی خط 4 داریم self رو بر می گردونیم. جلوتر راجع بهش صحبت می کنیم.
و اما __set__:

def __set__(self, instance, value: str):

این هم از امضای این متد (طبیعتا نباید چیزی برگردونه.) اما instance چیه؟ مجددا همین نمونه ای از کلاس که داریم یه ویژگیش رو به مقدار دلخواه خودمون تغییر می دیم. چون این شیء descriptor رو برای property هایی که str هستن استفاده می کنیم، مطمئنا تایپ value باید str باشه.

def __set__(self, instance, value: str): if value is None or value == '': raise ValueError(f'The {self.name} shouldn\'t be empty.') instance.__dict__[self.name] = value

و اما منطق لعنتی که توی نسخه ی noob نوشتن Person استفاده کردیم رو داریم اینجا به شکل pythonic تر می نویسیم. خداحافظ تکرار!
خب حالا از این چیزی که نوشتیم چجوری استفاده کنیم؟؟؟ بیا برگردیم به کلاس Person جدیدمون:

class Person: first_name = NonEmptyString('first_name') last_name = NonEmptyString('last_name') nickname = NonEmptyString('nickname') def __init__(self, first_name: str, last_name: str, nickname: str): self.first_name = first_name self.last_name = last_name self.nickname = nickname

بخش __init__ که کلا مشخصه. موقع ساخت شیء از کاربر first_name، last_name و nickname رو میگیره و برای self (همین instance)، به ویژگی تبدیل شون می کنه.
اما خط 2 تا 4 چی می خوان؟ و چرا این جوری هستن؟
ما در اصل داریم برای هر کدوم از این سه تا ویژگی، شیء descriptor می سازیم. و اگه به __init__ در NonEmptyString مون نگاه کنی، به نام ویژگی مون نیاز داریم برای هر نمونه از descriptor مون.
اما این که دستی باید اسم ویژگی رو بهش بدیم خودش باز هم یک مشکله.
که البته توی پست بعدی میگم :)


پس کد کامل تا اینجا:

class NonEmptyString: def __init__(self, name: str): self.name = name def __get__(self, instance, owner): if instance is None: print(f'Accessing the {self.name} directly from {owner.__name__}') return self print(f'Accessing the {self.name} from an instance of {owner.__name__}') return instance.__dict__[self.name] def __set__(self, instance, value: str): if value is None or value == '': raise ValueError(f'The {self.name} shouldn\'t be empty.') instance.__dict__[self.name] = value class Person: first_name = NonEmptyString('first_name') last_name = NonEmptyString('last_name') nickname = NonEmptyString('nickname') def __init__(self, first_name: str, last_name: str, nickname: str): self.first_name = first_name self.last_name = last_name self.nickname = nickname
python
۲
۰
DJZ
DJZ
اولین باری که کامپیوتری رو خراب کردم چون می خواستم ببینم یه قسمت خاصش چه شکلیه فقط یک ساله بودم. ولی مطمئن نیستم آخرین بارم باشه.
شاید از این پست‌ها خوشتان بیاید