کپسولهسازی و dataclass
در فصل قبل کلاس ساختیم ولی هر کسی میتوانست هر مقداری داخل صفتهایش بریزد. این فصل درباره محافظت از داده است، و درباره اینکه چطور کد تکراری را به پایتون بسپاریم.
۱. مشکل صفت آزاد
# Nothing stops the outside world from writing nonsense into an attribute.
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
s = Student('Ali', 18)
print(s.score)
s.score = 5000 # a score of 5000 out of 20?
s.score = -3 # a negative score?
s.score = 'excellent' # not even a number!
print(s.score)
مشکل: نمونه 01_public_attribute_problem.py
کپسولهسازی یعنی: کلاس، و فقط کلاس، تصمیم میگیرد چه مقداری معتبر است. شیء باید بتواند درباره داده خودش تضمین بدهد.
۲. راه حل کلاسیک: getter و setter
# The classic solution (the one used in Java or C#): hide the attribute behind
# two methods.
class Student:
def __init__(self, name, score=0):
self._name = name # a single underscore means "do not touch"
self._score = 0
self.set_score(score) # reuse the validation of the setter
def get_score(self):
return self._score
def set_score(self, value):
if not isinstance(value, (int, float)):
raise TypeError('the score must be a number')
if value < 0 or value > 20:
raise ValueError('the score must be between 0 and 20')
self._score = value
s = Student('Ali', 18)
print(s.get_score())
try:
s.set_score(5000)
except ValueError as e:
print('Refused:', e)
getter و setter دستی: نمونه 02_getters_setters.py
این کار میکند ولی زشت است: هر خواندن ساده به
s.get_score() تبدیل میشود. پایتون راه بهتری دارد. به
این نکته هم دقت کنید که سازنده خودش set_score را صدا
میزند تا اعتبارسنجی دوباره نوشته نشود.
۳. دکوراتور property
# @property turns a METHOD into something that is READ like an attribute.
class Student:
def __init__(self, name, score=0):
self._name = name
self._score = score
@property
def score(self):
'Reading s.score really calls this method.'
return self._score
@score.setter
def score(self, value):
'Writing s.score = ... really calls this method.'
if value < 0 or value > 20:
raise ValueError('the score must be between 0 and 20')
self._score = value
@property
def passed(self):
'A computed property: there is no attribute behind it.'
return self._score >= 10
s = Student('Ali', 18)
print(s.score) # looks like an attribute, but calls the getter
s.score = 19.5 # calls the setter
print(s.score)
print(s.passed) # computed on the fly
try:
s.score = 100
except ValueError as e:
print('Refused:', e)
دکوراتور property: نمونه 03_property.py
شرح مثال
@propertyیک متد را به چیزی تبدیل میکند که مثل صفت خوانده میشود. نه پرانتزی، نهget_ی.@score.setterمتد نوشتن را تعریف میکند. نام هر دو متد باید یکی باشد.- مقدار واقعی در
_scoreبا یک زیرخط نگه داشته میشود، تا با نام property تداخل نکند. passedفقط getter دارد، پس فقطخواندنی است. تلاش برای نوشتن روی آنAttributeErrorمیدهد.- بزرگترین مزیت: میتوانید کلاس را ساده شروع کنید و بعداً بدون تغییر هیچ کد بیرونی، اعتبارسنجی اضافه کنید. در جاوا باید همان اول getter و setter مینوشتید.
۴. اعتبارسنجی و صفت محاسباتی
'''A temperature that can be read in Celsius and in Fahrenheit.'''
class Temperature:
def __init__(self, celsius: float = 0.0) -> None:
self.celsius = celsius # goes through the setter, so it is checked
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError('below the absolute zero!')
self._celsius = value
@property
def fahrenheit(self) -> float:
'Computed from the Celsius value - nothing is stored.'
return self._celsius * 9 / 5 + 32
@fahrenheit.setter
def fahrenheit(self, value: float) -> None:
'Writing Fahrenheit updates the Celsius value.'
self.celsius = (value - 32) * 5 / 9
t = Temperature(25)
print(t.celsius, t.fahrenheit)
t.fahrenheit = 212
print(t.celsius) # 100.0
دما: نمونه 04_property_validation.py
شرح مثال
- فقط یک مقدار ذخیره میشود: سلسیوس. فارنهایت هر بار محاسبه میشود. پس هرگز نمیتوانند با هم ناسازگار شوند.
- نوشتن روی فارنهایت، مقدار سلسیوس را عوض میکند و چون از راه setter میرود، اعتبارسنجی هم انجام میشود.
- در سازنده هم
self.celsius = celsiusنوشته شده، نهself._celsius. این عمدی است: میخواهیم مقدار اولیه هم بررسی شود.
۵. صفتهای خصوصی
# Python has no real "private". It has two conventions instead.
class Demo:
def __init__(self):
self.public = 1 # anybody may use it
self._protected = 2 # convention: "internal, do not touch"
self.__private = 3 # name mangling: becomes _Demo__private
def show(self):
print(self.public, self._protected, self.__private)
d = Demo()
d.show()
print(d._protected) # works: it is only a convention
# print(d.__private) -> AttributeError
print(d._Demo__private) # it is still reachable if you really insist
خصوصیسازی: نمونه 05_private_attributes.py
| شکل | معنی | واقعاً محافظت میشود |
|---|---|---|
name | عمومی، هر کسی میتواند | - |
_name | «داخلی است، دست نزنید» | خیر، فقط قرارداد |
__name | نام دستکاری میشود | تقریباً، ولی قابل دور زدن |
پایتون هیچ صفت واقعاً خصوصی ندارد. فلسفهاش این است: «همه ما بزرگسالیم». یک زیرخط یعنی «این جزو رابط عمومی نیست و ممکن است فردا عوض شود؛ اگر استفادهاش کردی، مسئولیتش با خودت است». دو زیرخط هم برای امنیت نیست، بلکه برای جلوگیری از تصادم نام در وراثت است، که موضوع فصل بعد است.
۶. متد ایستا
# A @staticmethod is a plain function that we choose to store inside a class,
# because it belongs to the same subject. It receives neither self nor cls.
class Validator:
'A realistic use: a group of related checks.'
@staticmethod
def is_valid_email(text: str) -> bool:
return '@' in text and '.' in text.split('@')[-1]
@staticmethod
def is_valid_phone(text: str) -> bool:
return text.isdigit() and len(text) == 11
print(Validator.is_valid_email('ali@kadoosedu.ir'))
print(Validator.is_valid_phone('09121234567'))
متد ایستا: نمونه 06_static_method.py
متد ایستا نه self میگیرد و نه به شیء کار دارد. فقط تابعی
است که موضوعش به کلاس مربوط است، پس آنجا نگهش
میداریم. از راه خود کلاس صدا زده میشود و نیازی به ساختن شیء نیست.
۷. متد کلاس و سازنده جایگزین
# A @classmethod receives the CLASS itself as its first parameter ('cls').
# Its main use is to offer other ways of building an object.
class Student:
school = 'Kadoos' # class attribute
count = 0 # how many students were created
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
Student.count += 1 # shared counter
@classmethod
def from_string(cls, text: str) -> 'Student':
'Build a Student out of "name,age".'
name, age = text.split(',')
return cls(name.strip(), int(age))
@classmethod
def from_dict(cls, data: dict) -> 'Student':
'Build a Student out of a dictionary.'
return cls(data['name'], data['age'])
@classmethod
def change_school(cls, new_name: str) -> None:
'Change the class attribute for every student at once.'
cls.school = new_name
def describe(self) -> str:
return f'{self.name}, {self.age}, {self.school}'
s1 = Student('Ali', 20)
s2 = Student.from_string('Sara, 19')
s3 = Student.from_dict({'name': 'Reza', 'age': 22})
print('Students created:', Student.count)
متد کلاس: نمونه 07_class_method.py
| نوع متد | اولین پارامتر | به چه دسترسی دارد |
|---|---|---|
| معمولی | self | داده همان شیء |
@classmethod | cls | خود کلاس و صفتهای کلاس |
@staticmethod | هیچ | فقط آرگومانهای خودش |
شرح مثال
- سازنده جایگزین پرکاربردترین استفاده متد کلاس است. یک کلاس فقط یک
__init__دارد، ولی میتواند ده متدfrom_...داشته باشد. - داخل متد کلاس،
cls(...)یعنی «یک شیء از همین کلاس بساز». نوشتنclsبه جای نام کلاس مهم است: اگر کسی از این کلاس ارث ببرد، سازنده جایگزین برای او هم درست کار میکند. countیک صفت کلاس است که هر بار در سازنده یکی زیاد میشود. یک نمونه واقعی از جایی که صفت کلاس واقعاً به کار میآید.
۸. ترکیب
# Composition: an object HAS another object.
# It is usually a better idea than inheritance (session 23).
class Engine:
def __init__(self, power: int) -> None:
self.power = power
self.running = False
def start(self) -> None:
self.running = True
print(f'Engine of {self.power} hp started.')
def stop(self) -> None:
self.running = False
print('Engine stopped.')
class Car:
def __init__(self, brand: str, engine: Engine) -> None:
self.brand = brand
self.engine = engine # the car HAS an engine
def start(self) -> None:
print(f'{self.brand}:')
self.engine.start() # the work is delegated to the engine
def stop(self) -> None:
self.engine.stop()
engine = Engine(150)
car = Car('Mazda', engine)
car.start()
car.stop()
ترکیب: نمونه 08_composition.py
ترکیب یعنی شیئی، شیء دیگری را دارد. ماشین موتور دارد. کتابخانه کتاب دارد. سفارش، کالاها دارد. در فصل بعد وراثت را میبینید که یعنی شیئی یک نوع شیء دیگر است. قاعده حرفهای: هر جا شک کردید، ترکیب را انتخاب کنید.
۹. دکوراتور dataclass
# Writing __init__ by hand for a class that only holds data is tiring.
# @dataclass writes __init__, __repr__ and __eq__ for us.
from dataclasses import dataclass
@dataclass
class User:
name: str # the type hint is REQUIRED here
age: int
u1 = User('Adam', 25)
print(u1) # __repr__ for free: User(name='Adam', age=25)
u2 = User('Adam', 25)
# __eq__ for free: two dataclasses are equal when their fields are equal.
if u1 == u2:
print('They are the same person!')
# Without @dataclass the comparison would be False, because a normal class
# compares the identity of the objects, not their content.
class PlainUser:
def __init__(self, name, age):
self.name = name
self.age = age
print(PlainUser('Adam', 25) == PlainUser('Adam', 25)) # False
dataclass: نمونه 09_dataclass.py
User(name='Adam', age=25) They are the same person! False
شرح مثال
- یک خط
@dataclassسه چیز رایگان میدهد: سازنده، نمایش خوانا، و مقایسه بر اساس محتوا. - راهنمای نوع اینجا اجباری است. بدون آن، پایتون نمیفهمد کدام خط یک فیلد است.
- در کلاس معمولی،
==فقط میپرسد «آیا این دو، دقیقاً یک شیء هستند؟» و جوابش برای دو شیء متفاوت با محتوای یکسان،Falseاست. dataclass این را عوض میکند. - در فصل بعد میبینید که همه این موارد را میشود دستی هم نوشت؛
@dataclassفقط شما را از نوشتنشان معاف میکند.
۱۰. dataclass پیشرفته
from dataclasses import dataclass, field, asdict
@dataclass
class Product:
name: str
price: float = 0.0 # a default value
tags: list[str] = field(default_factory=list) # NEVER write tags: list = []
def with_vat(self, rate: float = 0.09) -> float:
'A dataclass can have normal methods too.'
return self.price * (1 + rate)
def __post_init__(self) -> None:
'__post_init__ runs right after the generated __init__: validate here.'
if self.price < 0:
raise ValueError('the price cannot be negative')
p1 = Product('Notebook', 15_000)
p2 = Product('Pencil')
p1.tags.append('paper')
print(p2.tags) # [] - each product has its OWN list
print(asdict(p1)) # turn it into a dictionary
# order=True generates <, <=, > and >= so the objects can be sorted.
@dataclass(order=True)
class Score:
value: int
student: str
scores = [Score(18, 'Ali'), Score(20, 'Sara'), Score(15, 'Reza')]
print(sorted(scores))
# frozen=True makes the object immutable (and hashable).
@dataclass(frozen=True)
class Point:
x: int
y: int
p = Point(1, 2)
# p.x = 5 -> FrozenInstanceError
print({Point(1, 2): 'origin-ish'}) # usable as a dictionary key
dataclass پیشرفته: نمونه 10_dataclass_advanced.py
| ابزار | وظیفه آن |
|---|---|
field(default_factory=list) | لیست تازه برای هر شیء. تنها راه درست |
__post_init__ | اعتبارسنجی، بعد از سازنده خودکار |
asdict(obj) | تبدیل به دیکشنری. برای JSON عالی است |
order=True | ساختن عملگرهای مقایسه، برای مرتبسازی |
frozen=True | شیء تغییرناپذیر و قابل استفاده به عنوان کلید |
نوشتن tags: list = [] در یک dataclass خطای صریح
میدهد؛ خوشبختانه پایتون اینجا جلویتان را میگیرد. راه درست
field(default_factory=list) است که برای هر شیء لیست
تازهای میسازد. این همان دام فصل سیزدهم و فصل بیست و یکم است.
۱۱. نمونه کامل: جانهای بازی
class Hearts:
MAX = 9 # class constant
SAVE_FILE = 'save.txt'
def __init__(self, value: int = 5) -> None:
self.value = value # goes through the setter
@property
def value(self) -> int:
return self._value
@value.setter
def value(self, new_value: int) -> None:
if not isinstance(new_value, int):
raise TypeError('the number of hearts must be a whole number')
# Keep the value inside 0 .. MAX instead of raising: a game should not
# crash because the player lost one life too many.
self._value = max(0, min(new_value, self.MAX))
@property
def is_alive(self) -> bool:
return self._value > 0
def add(self, amount: int = 1) -> None:
self.value = self._value + amount
def remove(self, amount: int = 1) -> None:
self.value = self._value - amount
def save(self) -> None:
'Write the current number of hearts into a file.'
with open(self.SAVE_FILE, 'w', encoding='utf-8') as f:
f.write(str(self._value))
@classmethod
def load(cls) -> 'Hearts':
'Build a Hearts object from the save file (or a new one).'
if not os.path.exists(cls.SAVE_FILE):
return cls()
try:
with open(cls.SAVE_FILE, encoding='utf-8') as f:
return cls(int(f.read().strip()))
except ValueError:
print('[warning] the save file is damaged, starting a new game')
return cls()
def __str__(self) -> str:
return 'Hearts: ' + '*' * self._value
کلاس جانها: نمونه 11_hearts_game_state.py
شرح مثال
این کلاس همه چیز این فصل را کنار هم دارد:
- ثابت کلاس:
MAXوSAVE_FILEبرای همه اشیاء یکساناند. - property با محدودسازی: به جای خطا دادن، مقدار را در بازه صفر تا
MAXنگه میدارد. این تصمیم طراحی است: بازی نباید به خاطر یک جان اضافه از کار بیفتد. در حساب بانکی برعکس بود و خطا دادن درستتر بود. - property فقطخواندنی:
is_aliveاز روی مقدار محاسبه میشود. - سازنده جایگزین:
loadیک متد کلاس است که یا از فایل میخواند یا شیء تازه میسازد. حتی فایل خراب را هم مدیریت میکند. - متد
__str__: تعیین میکند شیء موقعprintچطور نمایش داده شود. موضوع فصل بیست و چهارم. - همه متدها از راه
self.valueکار میکنند، نهself._value، تا اعتبارسنجی همیشه انجام شود.
پوشه S22-oop-encapsulation-and-dataclasses شامل
نمونههای 01_public_attribute_problem.py تا
11_hearts_game_state.py.
خلاصه فصل
| ابزار | وظیفه آن |
|---|---|
| کپسولهسازی | کلاس تصمیم میگیرد چه مقداری معتبر است |
@property | متدی که مثل صفت خوانده میشود |
@x.setter | متدی که موقع نوشتن روی صفت اجرا میشود |
| property بدون setter | صفت فقطخواندنی یا محاسباتی |
_x | «داخلی است». فقط قرارداد |
__x | دستکاری نام. برای جلوگیری از تصادم در وراثت |
@staticmethod | تابعی که موضوعش به کلاس مربوط است |
@classmethod | متدی با cls. برای سازنده جایگزین |
| ترکیب | شیئی که شیء دیگری را دارد |
@dataclass | سازنده و نمایش و مقایسه، رایگان |
field(default_factory=list) | تنها راه درست پیشفرض تغییرپذیر |
frozen=True | شیء تغییرناپذیر |
تمرینهای فصل بیست و دو
- به کلاس
Rectangleفصل قبل، property به نامareaاضافه کنید. - کلاس
Passwordرا بنویسید که setter آن رمز کوتاهتر از هشت کاراکتر را رد کند. - کلاس
Personرا با property فقطخواندنیfull_nameبنویسید. - به کلاس
Bookخود، متد کلاسfrom_string()اضافه کنید. - به همان کلاس، متد ایستای
is_valid_isbn()اضافه کنید. - کلاس
Bookرا به شکل@dataclassبازنویسی کنید. - یک
@dataclass(frozen=True)به نامCoordinateبنویسید و از آن به عنوان کلید دیکشنری استفاده کنید. - کلاس
Libraryرا بنویسید که لیستی ازBookها را دارد، با متدهایaddوremoveوfind_by_author.