پیشنیاز: آشنایی با شی گرایی و دکوریتور ها در پایتون
سلام دوستان امیدوارم روزهای خوبی رو سپری کنید
میخوام درباره 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 یک روش پایتونیک برای جلوگیری از نوشتن 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بسازیم:
# circle.py class Circle: def __init__(self, radius): self._radius = radius def _get_radius(self): print("Get radius") return self._radius def _set_radius(self, value): print("Set radius") self._radius = value def _del_radius(self): print("Delete radius") del self._radius radius = property( fget=_get_radius, fset=_set_radius, fdel=_del_radius, doc="The radius property." )
در قطعه کد بالا متد سازنده(__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 در نسخه 2.2 پایتون معرفی شده هنوز چیزی بنام دکوریتور در پایتون در دسترس نبود. بنابراین از روش قبلی که نوشتن تابع property بود استفاده می شد.
خب حالا میخواهیم مثال دایره رو با استفاده از دکوریتور property بازنویسی کنیم:
# circle.py class Circle: def __init__(self, radius): self._radius = radius @property def radius(self): """The radius property.""" print("Get radius") return self._radius @radius.setter def radius(self, value): print("Set radius") self._radius = value @radius.deleter def radius(self): print("Delete radius") 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است.
خب دوستان یک خلاصه ای از چیزهایی که گفتیم رو مرور کنیم:
property@
رو باید روی متد getter بذاریمخب حالا یه نکته :دوستان باید اجتناب کنیم از تبدیلattribute به property
زمانی که نیاز نداریم هیچ گونه پردازش اضافی رو attribute داشته باشیم.
استفاده از property
در جای نامناسب میتون کد شما رو:
مگر اینکه نیاز داشته باشید چیزی بجز attribute خالی داشته باشید،property ننویسید.property
بیجا باعث هدررفتن زمان cpuو زمان خودتون میشه.
بچه ها شاید ابتدایی ترین استفاده از 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("x coordinate is read-only") @property def y(self): return self._y @y.setter def y(self, value): raise WriteCoordinateError("y coordinate is read-only")
درمثال بالا اومدیم و exception رو شخصی سازی کردیم .
شما میتونید با استفاده از 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) را با تنظیماتی در متد (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("Password is write-only") @password.setter def password(self, plaintext): salt = os.urandom(32) self._hashed_password = hashlib.pbkdf2_hmac( "sha256", plaintext.encode("utf-8"), salt, 100_000 )
در کد بالا ما میاییم دوتا مقدار username , password رو در متد سازنده(__init__) از کاربر میگیریم.نام کاربری رو بصورت خام ذخیره میکنیم ولی پسورد رو با استفاده از الگوریتم خاصی هش میکنیم و بصورت خام ذخیره نمیکنیم و اجازه دیدن پسورد رو هم به کاربر نمیدیم.و هرزمان که کاربر خواست پسورد رو ببینه AttributeError
بهش نشون داده میشه.
حالا بیاید تست کنیم:
>>> from users import User >>> Mohammad = User("Mohammad", "secret") >>> 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 = "supersecret" >>> 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 ،برای اعتبار سنجی داده قبل از ذخیره شدن یا حتی قبول کردن داده بعنوان داده ایمن است.اعتبارسنجی داده یک نیاز متداول در کدهایی است که ورودیها را از کاربران یا منابع اطلاعاتی دیگری دریافت می کنند که ممکنه غیرقابل اعتمادباشند.
ویژگی 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("Validated!") except ValueError: raise ValueError('"x" 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("Validated!") except ValueError: raise ValueError('"y" must be a number') from None
خب بیایید درباره متد setter که برای x. نوشتیم صحبت کنیم:
با خاصیت try ..except سعی کردیم که بگیم تلاشتو بکن که x رو تبدیل به floatکنی و بعد ذخیره کنی اما اگر نتونستی یه استثناValueError
برگردون.برای y هم به همین شکل...
در مثال بالا ما اومدیم ازraise
…from 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 = "one" Traceback (most recent call last): ... ValueError: "x" must be a number >>> point.y = "1o" Traceback (most recent call last): ... ValueError: "y" 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("Validated!") except ValueError: raise ValueError(f'"{self._name}" 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"${self._price:,.2f}"
بعنوان آخرین مثال می خواهیم در کلاس 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 نهگداری کنیم .
گاهی اوقات شما یه سری 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
میگیریم.
شما میتونید یکسری 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__}("{self._data}")'
در مثال بالاTreeNode
یک گره در ساختار درختی ماهست.هر گره فرزندان خود را در یک لیست ذخیره میکند.متد deleter همه فرزندان یک گره رو پاک می کند.
>>> from tree import TreeNode >>> root = TreeNode("root") >>> child1 = TreeNode("child 1") >>> child2 = TreeNode("child 2") >>> root.children = [child1, child2] >>> root.children [TreeNode("child 1"), TreeNode("child 2")] >>> del root.children >>> root.children []
همانطور که میدونید _ میان متدهای فراخوندی رو تبدیل به 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 ضرب میکنیم و مقدار سنت که ازقبل داشتیم رو بهش اضافه میکنیم و بهمین ترتیب برای سنت ...
خب دوستان این اموزش هم به پایان رسید خیلی مطالب زیاد شد اما سعی کردم مطالب مهم رو بگم.
و درنهایت گوش جان میسپاریم به نظرات و انتقادات ارزشمند شما جهت بهبود آموزش ها
با تشکر
محمد محمودی