محمد محمودی
محمد محمودی
خواندن ۲۸ دقیقه·۸ ماه پیش

نگاهی به property در پایتون

با property در پایتون attributeهای مدیریت شده به کلاس های خود اضافه کنید.
با property در پایتون attributeهای مدیریت شده به کلاس های خود اضافه کنید.

با property در پایتون attributeهای مدیریت شده به کلاس های خود اضافه کنید.

پیشنیاز: آشنایی با شی گرایی و دکوریتور ها در پایتون
سلام دوستان امیدوارم روزهای خوبی رو سپری کنید
میخوام درباره property در پایتون صحبت کنم
بامن همراه باشید تا مروری بر اون چیزی که میخوام باهاتون صحبت کنم داشته باشیم:
شما حتما با attributes ها در پایتون آشنایی دارید(نیازه که داشته باشید) یک سری ویژگی ها که میخواهیم به کلاس هامون اضافه کنیم بهش میگم attributes.

در پایتون attributesها معمولا بصورت خام ذخیره میشن.حالا اگر ما بخواهیم کنترل روی attributesها داشته باشیم باید از attributesهای مدیریت شده یا همون property استفاده کنیم.

خب حالا بیایید ببینیم propertyچه کمکی به ما می کنه؟
فرض کنید یک کتابخانه یا یک کلاس نوشتید که خیلیا دارن ازش استفاده میکنن و حالا میخواین یکی از attributesها رو اصلاح کنید یا کنترل بذارید رو اون و میخواهید که کاربرانی که دارن از کلاس یا کتابخانه شما استفاده می کنن دچار مشکل نشن در این صورت بهترین راه استفاده از propertyاست.

حالا ما در این آموزش میخواهیم چی یاد بگیریم:
1.ساختن attributesهای مدیریت شده یا همون property در کلاس
2.انجام lazy attribute evaluation و ارائه computed attributes

3.جلوگیری استفاده از setter, getter برای اینکه پایتونی تر و تمیزتر (clean code)کد بزنیم
4.نوشتن property های read-only ,read-write, write-only
5.ساخت کلاس ها یا کتابخانه های استوار و قابل برگشت به عقب(backward-compatible)
خب دوستان یه توضیح ریز درباره backward-compatible بدم:
سازگاری به عقب --> قابلیت اجرای کد نوشته شده در نسخه های قدیمی تر

خب حالا بریم سراغ اولین بحث :attributesهای مدیریت شده در کلاس
زمانی که یک کلاس در زبان های شی گرا تعریف می کنیداحتمالا با یکسری از attributesهای کلاس یا نمونه یا شی مواجه خواهید شد.اگر بخواهیم بهتر بگیم وابسته به زبانی که استفاده می کنید شما با متغیر هایی که قابل دسترس از طریق نمونه و کلاس یا هردو باشند ،مواجه خواهید شد.
attributesها اغلب حالت داخلی شی یا مقادیر داخلی شی را یا نشان می دهند یا نگهداری می کنند که ما نیاز خواهیم داشت که دسترسی پیدا کنیم به انها یا تغییرشون بدیم.

معمولا حداقل دو راه برای مدیریت کردن attributesها وجود داره.یا اینکه مستقیما به attributesها دسترسی پیدا کنیم و تغییر بدیم یا اینکه از متد ها استفاده کنیم.متدها توابعی هستند که درون کلاس ها نوشته میشن و رفتارها و اعمالی را ارائه می دهن که یک شی می تونه با داده ها و ویژگی های داخلی خودش انجام بده.

اگر attributeها رو در معرض استفاده کاربران قرار بدید کاربر این attribute مستقیما در کد خود دسترسی و تغییر می ده و مشکل زمانی پیش میاد که نیاز به تغییر پیاده سازی داخلی یک attribute دارید.

حالا فرض کنید که ما یک کلاس داریم بنام دایره (Circle) و این کلاس یک attribute داره بنام شعاع(radius.)
و کلاس شما آماده شده و دراختیار کاربرنهایی قرار دادید و اون هم داره از کد استفاده میکنه و هیچ مشکلی هم وجود نداره .آفرین!!!!
حالا یکی از کاربر های مهم شما میاد و میگه که دیگه نمیخواییم شعاع رو ذخیره کنیم و میخواییم قطر را بعنوان attribute عمومی ذخیره کنیم.

حالا در این حالت حذف شعاعradius. و جایگزینی قطرdiameter. باعث شکستن کد و کار نکردن آن میشود.خب این مشکل را ماباید با راهی بجز حذف کردن حل کنیم.

زبان های برنامه نویسی مانند جاوا و C++ شما را تشویق میکنند که هیچ گاه attribute ها در معرض دسترسی کاربر قرار ندهید.در عوض شما باید متدهایی مانند getter, setter بنویسد جهت دسترسی و تغییر attribute ها.
این متدها اغلب راهی برای تغییر و دسترسی attribute ها بدون تغییر در کلاسی که کابران نهایی استفاده می کنن ارائه میده.
نکته:معمولا متدهای getter, setter به عنوان یک الگوی نادرست و نشانه‌ای از طراحی ضعیف شیء‌گرایی محسوب می‌شون.و دلیل اصلی هم اینکه encapsulation رو میشکنه و به شما اجازه دسترسی و تغییر کامپوننت ها رو میده.

روش Getterو Setter در پایتون
از نظر فنی کسی نمیتونه جلوی شما رو بگیره تا از getterو setter در پایتون استفاده نکنید.
خب حالابیاید تاببینم چجور باید استفاده کنیم:

# point.py class Point: def __init__(self, x, y): self._x = x self._y = y def get_x(self): return self._x def set_x(self, value): self._x = value def get_y(self): return self._y def set_y(self, value): self._y = value

در مثال بالا یک کلاس نوشتیم بنام نقطه که مختصات دکارتی نقطه رو مشخص میکنه که در این مثال x_.وy_. دو attributeهای خصوصی (private) ماهستند.

نکته:در پایتون چیزی بنام access modifiers نداریم access modifiers یعنی اصلاح کننده دسترسی مثل :
private, protected, public
در پایتون شما می تونید با گذاشتن علامت underscore یا _ قبل از نام متغیر آن متغیر رو تبدیل به متغیر خصوصی کنید یا باگذاشتن دوتا آندراسکور تبدیل به متغیر protectedکنید. اما اینها فقط یک قرارداد است و پایتون برنامه نویس رو به زور وادار نمیکنه که نتونه استفاده کنه از متغیر های private, protected و با dot notation امکان دسترسی و استفاده رو به برنامه نویس میده برای مثال obj._attr یک نمونه از دسترسی می باشد.

برای دسترسی و تغییر به x و y میتونیم از getter , setter استفاده کنیم .خب حالا میخوایم با استفاده از شل پایتون کدی که نوشتیم رو تست کنیم:

>>> from point import Point >>> point = Point(12, 5) >>> point.get_x() 12 >>> point.get_y() 5 >>> point.set_x(42) >>> point.get_x() 42 >>> # ویژگی های غیر عمومی هنوز در دسترس هستند >>> point._x 42 >>> point._y 5

روش پایتونیک
با اینکه مثالی که در قبل دیدید از سبک(سینتکس) کدنویسی پایتون استفاده می کند اما پایتونیک نیست.خب حالا میخواییم کدمون رو بازنویسی کنیم طوریکه متدهای getter, setter هیچ پردازشی در برنامه ندارند.

>>> class Point: ... def __init__(self, x, y): ... self.x = x ... self.y = y ... >>> point = Point(12, 5) >>> point.x 12 >>> point.y 5 >>> point.x = 42 >>> point.x 42

این کد یک اصل اساسی را آشکار می‌کند. ارائه attributeها به کاربران نهایی در پایتون طبیعی و رایج است. شما نیازی ندارید که همیشه کلاس‌های خود را با متدهای getter , setter پر کنید که خیلی خوب به نظر می‌رسد!
خب حالا باید چجوری تغییرات مورد نظرمون که ممکنه کلاس هایی که کابران نهایی استفاده میکنند رو هم درگیر میکنه اعمال کنیم؟

برخلاف جاوا و سی‌پلاس‌پلاس، پایتون ابزارهای مفیدی ارائه می‌ده که به شما امکان می‌ده تا پیاده‌سازی اصلی ویژگی‌های خود را بدون تغییرکلاس های عمومی‌تون تغییر بدید.محبوب ترین رویکردی که پایتون ارائه میده اینه که بیایم attributeهامون رو به properties تبدیل کنیم.

نکته :یکی دیگه از راه ها استفاده از discriptorsها هستش که ما در این اموزش تمرکزمون روی property هستش.

خب ببینید دوستان propertyها یک عملکرد میانی بین attributeهای ساده و متدها رو نشون میدن.درواقع بشما این اجازه رومیدن که یک سری متد هارو بسازید که رفتارشون شبیه attriubuteهاست.با استفاده از property می‌توانید نحوه محاسبه attributeهای مورد نظر خود را هر زمان که نیاز دارید تغییر دهید

بعنوان مثال ما میتوانیم x. و y. تبدیل به propertyکنیم.
همچنین، یک متد پایه وجود خواهد داشت که مقادیر .x و .y را نگه می‌داره و به شما امکان می‌ده تا پیاده‌سازی داخلی آن‌ها را تغییر بدید و عملیاتی را بر روی آن‌ها انجام بدید درست قبل از اینکه کاربران شما به آن‌ها دسترسی پیدا کنن و آن‌ها را تغییر بدهن.
نکته: propertyها محدود به پایتون نیستند. زبان‌هایی مانند جاوااسکریپت، C#، کاتلین و... همچنین ابزارها و تکنیک‌هایی را برای ایجاد propertyها به عنوان اعضای کلاس فراهم می‌کنند.

اصلی‌ترین مزیت propertyهای پایتون اینه که به شما اجازه می‌ده تا attributeهای خود را به عنوان بخش عمومی و در دسترس کلاس نشان بدید. اگر زمانی نیاز به تغییر اساسی در پیاده سازی داشتید میتونید با تبدیل attributeها به property بدون دردسر زیاد تغییرات رو اعمال کنید...

  • شروع کار با propertyهای پایتون

تا اینجا فهمیدیم که property یک روش پایتونیک برای جلوگیری از نوشتن getter و setter در کد است.تابع
()property این امکان رو میده که attributeهای کلاس رو تبدیل به propertyکنیم.
از ان جایی که property یک تابع داخلی پایتون است نیاز به ایمپورت ندارد و بخاطر پیاده سازی شدن با زبان C بهینه سازی شده است.

نکته:از انجایی که property یک تابع داخلی پایتون است اما propertyیک کلاس است که بعنوان تابع کار می کند.

با استفاده از property، می‌توانید متدهای getter , setter را به attributeهای کلاس اضافه کنید. به این ترتیب، می‌توانید پیاده‌سازی داخلی برای آن attribute را مدیریت کنید بدون اینکه متدهای getter , setterرا در کلاس خود بنمایش بگذارید. همچنین، می‌توانید یک روش برای مدیریت حذف attributeها مشخص کنید و برای attributeهای خود یک مستند متناسب فراهم کنید.

بیایید یه نگاهی به نحوه نگارش تابع property بندازیم:

property(fget=None, fset=None, fdel=None, doc=None)


آرگومان fget: تابعی است که یک مقدار از attribute رو برمیگرداند
آرگومان fset:تابعی که یه مقدار در attributeموردنظر ست میکند.
آرگومان fdel:تابعی که مقدار ست شده را حذف میکند.
آرگومان doc:داک‌استرینگی (docstring)که برای attribute موردنظر نیاز هست نوشته شود.

خب اگه شما بخواهید به مقدار property دسترسی پیدا کنید obj.attr پایتون بطور اتوماتیک تابع fgetرو صدا میزنه و اگر بخواهید مقداری رو ست کنید obj.attr = value پایتون بطور اتوماتیک fsetرو صدا میزنه. و درنهایت برای حذف del obj.attr تابع fdel رو صدا میزنه.

تونید از آرگومان doc برای نوشتن یک مستند مناسب برای propertyهای خود استفاده کنید. خودتون و همکاران برنامه‌نویس تون می‌تونید این مستند را با استفاده از help پایتون بخونید. همچنین، آرگومان doc وقتی که با ویرایشگرهای کد و IDEها که از دسترسی به مستندهای متنی پشتیبانی می‌کنن کار می‌کنی، مفیده.
شما می تونید property با دو روش استفاده کنید :

  • استفاده از دکوریتور
  • استفاده از تابع

دراین اموزش هر دو رو اموزش میدیم اما روش دکوریتور محبوب تر است.

ساخت attribute با property

خب در مثال زیر میخوایم یک attribute با propertyبسازیم:

# circle.py class Circle: def __init__(self, radius): self._radius = radius def _get_radius(self): print(&quotGet radius&quot) return self._radius def _set_radius(self, value): print(&quotSet radius&quot) self._radius = value def _del_radius(self): print(&quotDelete radius&quot) del self._radius radius = property( fget=_get_radius, fset=_set_radius, fdel=_del_radius, doc=&quotThe radius property.&quot )



در قطعه کد بالا متد سازنده(__init__) کلاس شعاع(radius) رو بعنوان ارگومان خصوصی (radius_.)جهت ذخیره دریافت می کند.بعد سه تابع خصوصی داریم :
تابع get_radius_ که مقدار radius_. برمی گردونه.
تابع set_radius_ که مقدار radius_. رو ست میکنه
تابع del_radius_ که attribute آبجکت رو حذف میکنه
وقتی این سه متد را در دسترس داشته باشید، یک attribute کلاس به نام .radius برای ذخیره شیءproperty ایجاد می‌کنید.
حالا برای ساخت property باید این سه متد رو بعنوان ارگومان به تابع propertyبدیم.
خب حالا بیاید کدی که نوشتیم رو تست کنیم:

>>> from circle import Circle >>> circle = Circle(42.0) >>> circle.radius Get radius 42.0 >>> circle.radius = 100.0 Set radius >>> circle.radius Get radius 100.0 >>> del circle.radius Delete radius >>> circle.radius Get radius Traceback (most recent call last): ... AttributeError: 'Circle' object has no attribute '_radius' >>> help(circle) Help on Circle in module __main__ object: class Circle(builtins.object) ... | radius | The radius property.

خب وقتی help گرفتیم دیدیم که property مربوط به radius اومده و attribute خصوصی کلاس رو پنهان کرده .

پس porpertyها attributeهای کلاس هستند که attribute های شی رو مدیریت می کنند.

>>> from circle import Circle >>> Circle.radius.fget <function Circle._get_radius at 0x7fba7e1d7d30> >>> Circle.radius.fset <function Circle._set_radius at 0x7fba7e1d78b0> >>> Circle.radius.fdel <function Circle._del_radius at 0x7fba7e1d7040> >>> dir(Circle.radius) [..., '__get__', ..., '__set__', ...]

خب ببینید دوستان propertyها override می کنن descriptorsها رو.
اگر ()dir استفاده کنیم برای دیدن عضو ها ، متدهای __set__ و __get__ دیده خواهند شد.

زمانی که ما متد setter برای property مورد نظر مشخص نکرده باشیم بطور پیش فرض __set__ فراخوانی میشود که در این صورت AttributeError دریافت خواهیم کرد.چون هیچ راهی برای ست کردن پیدا نمیکنه.

استفاده از property بعنوان دکوریتور

دکوراتورها در پایتون در همه‌جا هستن. آن‌ها توابعی هستن که یک تابع دیگر را به عنوان آرگومان می‌پذیرن و یک تابع جدید با قابلیت‌های اضافی برمی‌گردانن. با استفاده از یک دکوراتور، می‌توانید عملیات‌های پیش و پس از پردازش را به یک تابع موجود اضافه کنید.

زمانی که property در نسخه 2.2 پایتون معرفی شده هنوز چیزی بنام دکوریتور در پایتون در دسترس نبود. بنابراین از روش قبلی که نوشتن تابع property بود استفاده می شد.

خب حالا میخواهیم مثال دایره رو با استفاده از دکوریتور property بازنویسی کنیم:

# circle.py class Circle: def __init__(self, radius): self._radius = radius @property def radius(self): &quot&quot&quotThe radius property.&quot&quot&quot print(&quotGet radius&quot) return self._radius @radius.setter def radius(self, value): print(&quotSet radius&quot) self._radius = value @radius.deleter def radius(self): print(&quotDelete radius&quot) del self._radius

این کد بسیار زیباتر از روشی است که درآن متد های getter, setterرا می نویسیم.حالا کلاس Circle پایتونی تر و اصول کدنویسی تمیز در ان رعایت شده است.
دیگه نیاز نیست از متد هایی با نام های
()get_radius(), ._set_radius(), ._del_radius_.
استفاده کنیم.اولین متدی مینویسیم باید نام پابلیک (عمومی) همان attribtueباشد که قراره تبدیل به propertyبشه این متد باید منطق getterرا پیاده سازی کنه.دومین و سومین متد هم باید هم نام متد اول باشد با تفاوت هایی که در دکوریتور های انها دیده می شود.

>>> dir(Circle.radius) [..., 'deleter', ..., 'getter', 'setter']

در کنار fget, .fset, .fdel. و سایر attributeها و متدها propertyاین سه متد
(()deleter(), .getter(), and .setter.)
رو میسازه و هر کدوم یک property جدید برمیگردونه(return میکنه)
حالا وقتی شما دومین متد ()radius. دکوریت میکنید با radius.setter@درواقع دارید یک ویژگی جدید می سازید و به radius.که در خط 8 کد بالا نوشتیم اضافه می کنید. حالا این propertyیا همون ویژگی که ایجاد کردیم مجموعه ای از متد ها رو داره یکیش خط 8 و یکیش خط 14.

مکانیزم کارکردradius.deleter@ این به شکل بالا می باشد.

خب دوستان بیاید تا کدی که در بالا با استفاده از دکوریتور نوشتیم تست کنیم:

>>> from circle import Circle >>> circle = Circle(42.0) >>> circle.radius Get radius 42.0 >>> circle.radius = 100.0 Set radius >>> circle.radius Get radius 100.0 >>> del circle.radius Delete radius >>> circle.radius Get radius Traceback (most recent call last): ... AttributeError: 'Circle' object has no attribute '_radius' >>> help(circle) Help on Circle in module __main__ object: class Circle(builtins.object) ... | radius | The radius property.

با توجه به تستی که در بالا انجام دادیم دیگه نیاز به پرانتز برای فراخوانیradius. شبیه متدها نداریم و میتوانیم شبیه دسترسی به attribute ها که داشتیم انها فراخوانی کنیم که این اصلی ترین ویژگی استفاده از propertyاست.

خب دوستان یک خلاصه ای از چیزهایی که گفتیم رو مرور کنیم:

  1. دکوریتورproperty@ رو باید روی متد getter بذاریم
  2. مستندات یا همون docstring حتما باید در متد getterنوشته بشهپ
  3. متد های setter , deleterباید دکوریت بشن با نام متدی که برای getter در نظرگرفتیم بعلاوه setter. و getter.

خب حالا یه نکته :دوستان باید اجتناب کنیم از تبدیلattribute به property زمانی که نیاز نداریم هیچ گونه پردازش اضافی رو attribute داشته باشیم.

استفاده از property در جای نامناسب میتون کد شما رو:

  1. بی جهت شلوغ کنه
  2. برنامه نویس های دیگه رو گیج کنه
  3. کند تر کنه

مگر اینکه نیاز داشته باشید چیزی بجز attribute خالی داشته باشید،property ننویسید.property بیجا باعث هدررفتن زمان cpuو زمان خودتون میشه.


خب حالا میخواهیم attributeهای read-only بنویسیم

بچه ها شاید ابتدایی ترین استفاده از property ساختن attributeهای read-only باشه.
خب حالا فرض کنید شما میخواهید یک کلاس غیر قابل تغییر (immutable) بنام نقطه(Point) داشته باشید که به کاربر اجازه تغییر مختصات رو نده برای این کار کلاسمون رو به شکل زیر مینویسیم:

# point.py class Point: def __init__(self, x, y): self._x = x self._y = y @property def x(self): return self._x @property def y(self): return self._y

در اینجا ما اومدیم دوتا ورودی از کاربر گرفتیم و بعنوان attributeهای x_.و y_. ذخیره کردیم و همانطور که قبلا یادگرفتیم استفاده از underscore این مفهوم به سایر دولوپر ها میگه که این attributeها غیرعمومی(non-public) هستند و برای دسترسی به انها نباید از dot notiation مانند point._x استفاده کرد.و درنهایت ماباید با استفاده property@ یک متد getterبرای دسترسی به آن بنویسیم.

>>> from point import Point >>> point = Point(12, 5) >>> # Read coordinates >>> point.x 12 >>> point.y 5 >>> # Write coordinates >>> point.x = 42 Traceback (most recent call last): ... AttributeError: can't set attribute

خب دوستان ببیند getterما بدرستی کار میکنه و موقع ست کردن attribtue به خطا خوردیم چرا؟
قبلا کمی درباره discriptorها صبحت کردیم رفتارpropertyها بسیار متکی به descriptor هست که این property رو میسازه و دراین جا پیش فرض دیسکریپتور ()__set__.خطای AttributeError است تازمانی که بیایم و setter رو تعریف کنیم.

حالا کد بالا رو یه کم بهبود می بخشیم:

# point.py class WriteCoordinateError(Exception): pass class Point: def __init__(self, x, y): self._x = x self._y = y @property def x(self): return self._x @x.setter def x(self, value): raise WriteCoordinateError(&quotx coordinate is read-only&quot) @property def y(self): return self._y @y.setter def y(self, value): raise WriteCoordinateError(&quoty coordinate is read-only&quot)

درمثال بالا اومدیم و exception رو شخصی سازی کردیم .

خب حالا میخواهیم attributeهای read-write بسازیم:

شما میتونید با استفاده از property بیاید و attributeهایی که قابلیت خوندن و نوشتن دارند بنویسید.حالا ما میخواییم تو این تمرین getter مناسب برای read و setter مناسب برای write بنویسیم.

خب حالا فرض کنید برای کلاس دایره (Circle) که قبلا نوشتید یک attribute قُطر(diameter.) هم اضافه کنید.گرفتن قطر بعنوان ورودی در متد سازنده(__init__) ضروری نیست و با داشتن یکی میتونیم اون یکی رو محاسبه کنیم.

# circle.py import math class Circle: def __init__(self, radius): self.radius = radius @property def radius(self): return self._radius @radius.setter def radius(self, value): self._radius = float(value) @property def diameter(self): return self.radius * 2 @diameter.setter def diameter(self, value): self.radius = value / 2

خب تو مثال بالا ما اومدیم یک کلاس دایره نوشتیم که radius. در آن قابلیت read-write دارد.اگر برگردید به عقب و کلاس Circle که نوشته بودیم رو نگاه کنید میبینید که با این کلاس جدید یه تفاوت ریز داره.
بریم که توضیح بدیم داستان از چه قراره...

اگر دقت کنید یک نکته ظریف اینجا وجود داره و اونم اینه که ما مثل قبل مقداری که هنگام ساختن شی(آبجکت) ارسال میشه به خود property شعاع نسبت میدیم در صورتی که در مثال های قبل در یک متغیر خصوصی ذخیره میکردیم.خب علت این کار چیه؟

ما برای اینکه موقع ساخت آبجکت هم مقدار ارسالی از کاربر رو تبدیل به float کنیم نیاز داریم که متد setterهنگام ساخت آبجکت هم فراخوانی بشه بخاطر همین میاییم مقدار ارسالی کاربر مستقیما بخود property نسبت میدیم که کنترل ها و محاسبات لازم در موقع ساخت ابجکت هم اعمال بشه.

و در نهایت برای محاسبه قطر هم از خوده property شعاع استفاده می کنیم.

حالا بیایید کد رو تست کنیم:

>>> from circle import Circle >>> circle = Circle(42) >>> circle.radius 42.0 >>> circle.diameter 84.0 >>> circle.diameter = 100 >>> circle.diameter 100.0 >>> circle.radius 50.0

خب حالا میخواییم attributeهای فقط نوشتنی(Write-Only) بنویسیم:

شما همچنین می‌توانید attributeهای فقط‌نوشتنی (write-only) را با تنظیماتی در متد (getter) در propertyهای خود پیاده‌سازی کنید. به عنوان مثال، می‌توانید متد (getter) را به گونه‌ای تغییر دهید که هر زمان کاربر خواست به مقدار attribute دسترسی داشته باشد، یک استثناء ایجاد شود.

خب مثال زیر یک کد برای هندل کردن پسورد است:

# users.py import hashlib import os class User: def __init__(self, name, password): self.name = name self.password = password @property def password(self): raise AttributeError(&quotPassword is write-only&quot) @password.setter def password(self, plaintext): salt = os.urandom(32) self._hashed_password = hashlib.pbkdf2_hmac( &quotsha256&quot, plaintext.encode(&quotutf-8&quot), salt, 100_000 )

در کد بالا ما میاییم دوتا مقدار username , password رو در متد سازنده(__init__) از کاربر میگیریم.نام کاربری رو بصورت خام ذخیره میکنیم ولی پسورد رو با استفاده از الگوریتم خاصی هش میکنیم و بصورت خام ذخیره نمیکنیم و اجازه دیدن پسورد رو هم به کاربر نمیدیم.و هرزمان که کاربر خواست پسورد رو ببینه AttributeError بهش نشون داده میشه.

حالا بیاید تست کنیم:

>>> from users import User >>> Mohammad = User(&quotMohammad&quot, &quotsecret&quot) >>> Mohammad._hashed_password b'b\xc7^ai\x9f3\xd2g ... \x89^-\x92\xbe\xe6' >>> Mohammad.password Traceback (most recent call last): ... AttributeError: Password is write-only >>> Mohammad.password = &quotsupersecret&quot >>> Mohammad._hashed_password b'\xe9l$\x9f\xaf\x9d ... b\xe8\xc8\xfcaU\r_'

در این مثال ما میاییم و یک instanceیا شی از کلاس بنام Mohammad میسازیم و پسورد رو هم موقع ساختن شی میدیم.حالا موقعی که ما میخواییم به hashed_password_. دسترسی پیدا کنیم مشکلی نیست و پسورد رو بصورت هش نمایش میده اما اگر بخواییم بدونیم که پسورد اصلی ذخیره شده چیه به ما خطا
Password is write-only میده.و همین رفتار بعد از ست کردن پسورد جدید اتفاق می افتد.

توضیح کوتاه:os.urandom یک استرینگ 32 بیتی تولید میکنه.که این رو بعنوان نمک درنظر میگیریم.با استفاده از کتابخانه hashlib و الگوریتم SHA-256 و نمکی که تولید کردیم پسورد ما به اندازه 100000 بار
با نمک ترکیب میشه و یک هش جدید تولید میشه.

اعمال ویژگی ()property در پایتون

دوستان تا اینجا بیشتر مواردی که مربوط به propertyبود رو یاد گرفتید دو روش استفاده از دکوریتور و تابع رو یادگرفتید با تفاوت هاش.حالا میخواییم برای درک بیشتر مثال بیشتری حل کنیم ...

  1. اعتبار سنجی ورودی ها

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

ویژگی property در پایتون ابزاری سریع و قابل اعتماد برای رسیدگی به اعتبارسنجی داده‌های ورودی فراهم می‌کند.برای مثال برگردید به کلاس نقطه(Point) که نوشتیم در این کلاسی که نوشته شده کاربر آزاده که هر نوع داده ای که میخواد وارد کنه خب حالا میتونیم این اعتبار سنجی رو انجام بدیم تا کاربر موظف باشه حتما داده عددی وارد کنه.

# point.py class Point: def __init__(self, x, y): self.x = x self.y = y @property def x(self): return self._x @x.setter def x(self, value): try: self._x = float(value) print(&quotValidated!&quot) except ValueError: raise ValueError('&quotx&quot must be a number') from None @property def y(self): return self._y @y.setter def y(self, value): try: self._y = float(value) print(&quotValidated!&quot) except ValueError: raise ValueError('&quoty&quot must be a number') from None

خب بیایید درباره متد setter که برای x. نوشتیم صحبت کنیم:

با خاصیت try ..except سعی کردیم که بگیم تلاشتو بکن که x رو تبدیل به floatکنی و بعد ذخیره کنی اما اگر نتونستی یه استثناValueError برگردون.برای y هم به همین شکل...

در مثال بالا ما اومدیم از raisefrom None استفاده کردیم چرا؟ این سینتکس نوشتاری میاد جزئیات مربوط به استثنایی که رخ داده رو پنهان میکنه.از دیدگاه کاربران نهایی، این جزئیات ممکنه گیج‌کننده باشه و کلاس شما را ناقص به نظر بیاره.

خب حالا کلاسی که نوشتیم چجور کار میکنه؟

>>> from point import Point >>> point = Point(12, 5) Validated! Validated! >>> point.x 12.0 >>> point.y 5.0 >>> point.x = 42 Validated! >>> point.x 42.0 >>> point.y = 100.0 Validated! >>> point.y 100.0 >>> point.x = &quotone&quot Traceback (most recent call last): ... ValueError: &quotx&quot must be a number >>> point.y = &quot1o&quot Traceback (most recent call last): ... ValueError: &quoty&quot must be a number

تست کد قبل که در بالا امده به نظر میاد کامل گویا باشه نیاز به توضیح نداشته باشه.اما کد قبلی که نوشتیم یه اشکال ریز داره و اونم اینه که کد تکراری داریم و این با اصل DRY در پایتون مغایرت دارد .کد قبل رو بصورت زیر بهبود میدیم اما برای درک کد زیر نیاز هست درباره discriptorها در پایتون بدونید که خودش مبحث جدایی و نمی گنجه که در این اموزش توضیح بدم و فقط کد رو میزارم ...

# point.py class Coordinate: def __set_name__(self, owner, name): self._name = name def __get__(self, instance, owner): return instance.__dict__[self._name] def __set__(self, instance, value): try: instance.__dict__[self._name] = float(value) print(&quotValidated!&quot) except ValueError: raise ValueError(f'&quot{self._name}&quot must be a number') from None class Point: x = Coordinate() y = Coordinate() def __init__(self, x, y): self.x = x self.y = y

خب میریم سراغ کاربرد بعدی ...

2.بریم سراغ attributeهای محاسبه شده

اگر نیاز به یک attribute دارید که موقع دسترسی به آن بصورت پویا ساخته (محاسبه )بشه propertyمیتونه کمکتون کنه.به این نوع از attributeها معمولا میگن computed attributes.این نوع attributerها اماده(eager) بنظر میرسند درصورتی خیلی تنبل(lazy)هستند تا موقعی که صدا زده نشن هیچ محاسبه ای انجام نمیشه .

دلیل اصلی برای ایجاد (eager attributes) بهینه‌سازی هزینه محاسباتی است زمانی که به طور مکرر به attribueها دسترسی می‌یابید. از سوی دیگر، اگر به اندازه کمی از یک attribute خاص استفاده می‌کنید، در این صورت یک (lazy property) می‌تواند محاسبات آن را تا زمان لزوم به تأخیر بیندازد، که می‌تواند کارایی برنامه‌های شما را افزایش دهد.

به کلاس مستطیل زیر نگاه کنید:

class Rectangle: def __init__(self, width, height): self.width = width self.height = height @property def area(self): return self.width * self.height

در کد بالا مساحت مستطیل تازمانی که صدا زده نشه محاسبه نمیشه.

یکی از کاربردهای دیگه auto-formatted هستش که فرمت خروجی رو متناسب میکنه به مثال زیر توجه کنید:
class Product: def __init__(self, name, price): self._name = name self._price = float(price) @property def price(self): return f&quot${self._price:,.2f}&quot

بعنوان آخرین مثال می خواهیم در کلاس Point علاوه بر مختصات دکارتی ،مختصات قطبی هم داشته باشیم.

سیستم مختصات قطبی، هر نقطه را با استفاده از فاصله تا مبدأ و زاویه با محور افقی مختصات نشان می‌دهد.

# point.py import math class Point: def __init__(self, x, y): self.x = x self.y = y @property def distance(self): return round(math.dist((0, 0), (self.x, self.y))) @property def angle(self): return round(math.degrees(math.atan(self.y / self.x)), 1) def as_cartesian(self): return self.x, self.y def as_polar(self): return self.distance, self.angle

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

>>> from point import Point >>> point = Point(12, 5) >>> point.x 12 >>> point.y 5 >>> point.distance 13 >>> point.angle 22.6 >>> point.as_cartesian() (12, 5) >>> point.as_polar() (13, 22.6)

زمانی که قراره ما یک attribute محاسبه شده یا تنبل داشته باشیم propertyیک ابزار بسیار مفید است.پس با این حال اگر یک attribute داشته باشید که بسیار استفاده میشه محاسبه آن هر لحظه بسیار هزینه بر و بی فایده است.بهترین استراتژی این است که بعد از انجام محاسبات آنرا در حافظه cache نهگداری کنیم .

ذخیره (caching) attributeهای محاسبه شده

گاهی اوقات شما یه سری attribute های محاسبه شده داری که خیلی ازشون استفاده میکنی.تکرار مدام همان محاسبات ممکن است غیر ضروری و پر هزینه باشد.برای حل این مشکل بهتر یک attribute اختصاصی و خصوصی(private)برای ذخیره مقدار محاسبه شده داشته باشی برای استفاده های مجدد .

برای جلوگیری از رفتار غیرمنتظره ،به تغییر پذیری داده ورودی فکر کرده باشید.اگر شما یک property ساختید که مقدار محاسبه شده رو از یک داده ثابت ورودی محاسبه میکنه عملا خروجی کد شما تغییر نخواهد کرد.

مثال زیر مقدار را فقط یکبار محاسبه میکند:

# circle.py from time import sleep class Circle: def __init__(self, radius): self.radius = radius self._diameter = None @property def diameter(self): if self._diameter is None: sleep(0.5) # Simulate a costly computation self._diameter = self.radius * 2 return self._diameter

کد بالا یک اشکال دارد و اون هم اینه که در مرحله اول diameter.به درستی محاسبه میکنه و تو این کد ما یه کم تاخیر شبیه سازی کردیم با sleep.اما اگربیاییم و مقدار radius. را تغییر دهیم مقدارdiameter. تغییر نخواهد کرد.

>>> from circle import Circle >>> circle = Circle(42.0) >>> circle.radius 42.0 >>> circle.diameter # با تاخیر 84.0 >>> circle.diameter # بدون تاخیر 84.0 >>> circle.radius = 100.0 >>> circle.diameter # قطر نادرست 84.0

کد بالا گویای همه چیز است حالا میخواهیم کد رو جوری تغییر دهیم که در صورت تغییر radius. مقدار diameter.دوباره محاسبه گردد.

# circle.py from time import sleep class Circle: def __init__(self, radius): self.radius = radius @property def radius(self): return self._radius @radius.setter def radius(self, value): self._diameter = None self._radius = value @property def diameter(self): if self._diameter is None: sleep(0.5) # Simulate a costly computation self._diameter = self._radius * 2 return self._diameter

خب چه تغییراتی باعث درست کارکردن کد ما شد؟
ما میاییم در setter مربوط به radius. مقدار diameter. ریست میکنیم به None هرزمانی که مقدار radiusتغییر کند.با همین آپدیت کردن کوچک مقدار diameter. دوباره محاسبه میگردد.

>>> from circle import Circle >>> circle = Circle(42.0) >>> circle.radius 42.0 >>> circle.diameter # With delay 84.0 >>> circle.diameter # Without delay 84.0 >>> circle.radius = 100.0 >>> circle.diameter # With delay 200.0 >>> circle.diameter # Without delay 200.0

حالا کلاس Circleدیگه به درستی کار میکند.

خب یه گزینه دیگه برای cacheکردن attribute داریم خب بنظرتون چیه؟

استفاده از functools.cached_property از کتابخانه استاندارد پایتون.این تابع بصورت دکوریتور رو متد کار میکنه و بشما اجازه میده که یک متد رو تبدیل به cached property کنید.propertyفقط یک بار مقدار خود را محاسبه می‌کند و آن را به عنوان یک attribute عادی در طول عمر شی ذخیره می‌کند.

# circle.py from functools import cached_property from time import sleep class Circle: def __init__(self, radius): self.radius = radius @cached_property def diameter(self): sleep(0.5) # Simulate a costly computation return self.radius * 2

دراینجاdiameter. برای اولین بار که درخواست دسترسی بهش میشه محاسبه و ذخیره میشه .اما این پیاده سازی فقط برای زمانی جواب میده که مقدار ورودی ما تغییر نمیکنه.
>>> from circle import Circle >>> circle = Circle(42.0) >>> circle.diameter # With delay 84.0 >>> circle.diameter # Without delay 84.0 >>> circle.radius = 100 >>> circle.diameter # Wrong diameter 84.0 >>> # Allow direct assignment >>> circle.diameter = 200 >>> circle.diameter # Cached value 200

زمانی که شما به diameter. دسترسی پیدا می کنید،مقدار محاسبه شده را دریافت می کنید.بعداز این مقدار دیگه ثابت می مونه.با این حال برخلاف propertyدرcached_property اجازه ست کردن مستقیم رو همانطور که در کد بالا میبینیم میده مگر اینکه setterمتناسب رو نوشته باشیم.

خب حالا اگر میخوایید یک cached property بسازید که درمثال بالا اجازه تغییر مستقیم diameter. نده باید کدتون رو به شکل زیر اصلاح کنید:

# circle.py from functools import cache from time import sleep class Circle: def __init__(self, radius): self.radius = radius @property @cache def diameter(self): sleep(0.5) # Simulate a costly computation return self.radius * 2

ترکیب دو دکوریتور property@وcache@یکcached property میسازه که اجازه تغییر نمیده.

>>> from circle import Circle >>> circle = Circle(42.0) >>> circle.diameter # With delay 84.0 >>> circle.diameter # Without delay 84.0 >>> circle.radius = 100 >>> circle.diameter 84.0 >>> circle.diameter = 200 Traceback (most recent call last): ... AttributeError: can't set attribute

در کد بالا مشخصه که اگر مابخواهیم بصورت مستقیم مقدار دهیم کنیم AttributeError میگیریم.

مدیریت کردن attributeمربوط به Deletion

شما میتونید یکسری propertyهای بسازید که عملکرد deleting رو پیاده سازی کنه.ممکنه خیلی کم پیش بیاد که برای این کار از property استفاده کنید.اما داشتن یک راه برای دیلیت کردن ممکنه در برخی جاها مفید باشه.

فرض کنید که شما یک نوع داده درختی(tree) پیاده سازی کردید.درخت یک نوع داده ای است که داده ها رو بصورت سلسله مراتبی ذخیره میکنه. که به هرکدام از اجزاش میگن گره(node).هر گره در درخت یک parentداره و گره ای که parent نداشته بشه بهش ریشه یا root درخت میگن.هر گره میتون فرزند نداشته باشه یا بیش از یکی داشته باشه.

خب حالا فرض کنید شما یک راه نیاز دارید که بیاد فرزندان یک گره را خذف یا پاک کنه.در اینجا یک مثال داریم که یک گره درختی را پیاده‌سازی می‌کنه که از property برای ارائه بیشتر عملکردهای خود استفاده می‌کنه، از جمله امکان پاک کردن لیست فرزندان گره مورد نظر.

# tree.py class TreeNode: def __init__(self, data): self._data = data self._children = [] @property def children(self): return self._children @children.setter def children(self, value): if isinstance(value, list): self._children = value else: del self.children self._children.append(value) @children.deleter def children(self): self._children.clear() def __repr__(self): return f'{self.__class__.__name__}(&quot{self._data}&quot)'

در مثال بالاTreeNode یک گره در ساختار درختی ماهست.هر گره فرزندان خود را در یک لیست ذخیره میکند.متد deleter همه فرزندان یک گره رو پاک می کند.

>>> from tree import TreeNode >>> root = TreeNode(&quotroot&quot) >>> child1 = TreeNode(&quotchild 1&quot) >>> child2 = TreeNode(&quotchild 2&quot) >>> root.children = [child1, child2] >>> root.children [TreeNode(&quotchild 1&quot), TreeNode(&quotchild 2&quot)] >>> del root.children >>> root.children []

ساختن کلاس های سازگار به عقب(Backward-Compatible)

همانطور که میدونید _ میان متدهای فراخوندی رو تبدیل به attributeهایی میکنن که قابلیت دسترسی مستقیم دارند.این ویژگی به شما اجازه میده کدهای تمیزبنویسید.حالا شما میتونید attributeهای عمومی خودتون رو بدون setterو getter انتشار بدید.

فرض کنیدشما یک برنامه حسابداری برای مدیریت ارز نوشتید و دراختیار کاربران نهایی قرار دادید:

class Currency: def __init__(self, units, cents): self.units = units self.cents = cents

نکته خارج از بحث:
دوستان سیستم پولی ایالات متحده امریکا مبتنی بر ده دهی است، و واحد اصلی پول این کشور دلار (Dollar) و واحد جزء این کشور سنت (Cent) نامیده می شود یعنی هر 100 سنت یک دلار است.
در مثال بالا فرض کنید که شما 10 دلارو 20 سنت دارید 10 در units مقدار دهی میشه و 20 در cents.

کد بالا بسیار تمیز نوشته شده است.حالا فرض کنید نیازمندی های شما تغییر می کند و تصمیم میگیرید که تعداد کل سنت ها رو ذخیره کنید.حالا اگر بخواهیم cents و unitsپاک کنیم و بجاش از total_cents استفاده کنیم کاربران نهایی که از این کلاس استفاده می کردن به مشکل خواهند خورد.راه حل استفاده ازproperty است:

# currency.py CENTS_PER_UNIT = 100 class Currency: def __init__(self, units, cents): self._total_cents = units * CENTS_PER_UNIT + cents @property def units(self): return self._total_cents // CENTS_PER_UNIT @units.setter def units(self, value): self._total_cents = self.cents + value * CENTS_PER_UNIT @property def cents(self): return self._total_cents % CENTS_PER_UNIT @cents.setter def cents(self, value): self._total_cents = self.units * CENTS_PER_UNIT + value

یه توضیح درباره کد بالا:زمانی که کاربر مقدار در موقع ساخت شی، دلار و سنت وارد میکنهtotal_cents میسازیم درواقع عمل تبدیل رو انجام میدیم .خب حالا فرض کاربر تصمیم میگیره مقدار دلار رو عوض کنه دراین صورت مقدار دلار رو در 100 ضرب میکنیم و مقدار سنت که ازقبل داشتیم رو بهش اضافه میکنیم و بهمین ترتیب برای سنت ...

خب دوستان این اموزش هم به پایان رسید خیلی مطالب زیاد شد اما سعی کردم مطالب مهم رو بگم.
و درنهایت گوش جان میسپاریم به نظرات و انتقادات ارزشمند شما جهت بهبود آموزش ها
با تشکر
محمد محمودی

pythonpython programmingpropertyattributeپایتون
شاید از این پست‌ها خوشتان بیاید