برنامه‌نویسی پایتون فهرست مطالب
فصل ۲۲

کپسوله‌سازی و dataclass

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

در این فصل می‌خوانید
  1. مشکل صفت آزاد
  2. راه حل کلاسیک: getter و setter
  3. دکوراتور property
  4. اعتبارسنجی و صفت محاسباتی
  5. صفت‌های خصوصی
  6. متد ایستا
  7. متد کلاس و سازنده جایگزین
  8. ترکیب
  9. دکوراتور dataclass
  10. dataclass پیشرفته
  11. نمونه کامل: جان‌های بازی

۱. مشکل صفت آزاد

# 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

شرح مثال

۴. اعتبارسنجی و صفت محاسباتی

'''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

شرح مثال

۵. صفت‌های خصوصی

# 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داده همان شیء
@classmethodclsخود کلاس و صفت‌های کلاس
@staticmethodهیچفقط آرگومان‌های خودش

شرح مثال

۸. ترکیب

# 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 پیشرفته

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

شرح مثال

این کلاس همه چیز این فصل را کنار هم دارد:

فایل‌های تمرینی این فصل

پوشه 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شیء تغییرناپذیر

تمرین‌های فصل بیست و دو

  1. به کلاس Rectangle فصل قبل، property به نام area اضافه کنید.
  2. کلاس Password را بنویسید که setter آن رمز کوتاه‌تر از هشت کاراکتر را رد کند.
  3. کلاس Person را با property فقط‌خواندنی full_name بنویسید.
  4. به کلاس Book خود، متد کلاس from_string() اضافه کنید.
  5. به همان کلاس، متد ایستای is_valid_isbn() اضافه کنید.
  6. کلاس Book را به شکل @dataclass بازنویسی کنید.
  7. یک @dataclass(frozen=True) به نام Coordinate بنویسید و از آن به عنوان کلید دیکشنری استفاده کنید.
  8. کلاس Library را بنویسید که لیستی از Book ها را دارد، با متدهای add و remove و find_by_author.
فصل قبل: شیءگرایی فصل بعد: وراثت و چندریختی