Към съдържанието
wannacode

Капсулация

Проблем

Минус 500 €

В миналия урок сложихме проверката за теглене в метод. Само че полето е отворено и всеки може да пише в него направо:

account = Account("Мария", 100)
account.withdraw(500)
account.balance = -500
print(account.balance)
Изход
-500

Методът правилно отказа да изтегли 500 €. Следващият ред просто записа баланса на ръка. Проверката е там, но може да се заобиколи. Трябва начин да кажем „балансът се пипа само от сметката“.

Идеята

_balance: вътрешно за класа

Сложи пред името на полето долна черта: _balance.

class Account:
def __init__(self, owner, balance):
self._owner = owner
self._balance = balance
# ...

В Python няма private. Долната черта е договорка между програмистите: „това е вътрешно за класа, отвън не го пипай“. Python не спира никого. Редът

account._balance = -500

минава и балансът става -500. Но всеки, който го чете, вижда, че правилото е нарушено (PyCharm например го подчертава). Кодът извън класа ползва само методите на класа.

Класът крие данните си и ги показва само през свои методи. Това се казва капсулация. В Java и C# за това има думи, модификатори за достъп (private, public). В Python тази роля има долната черта.

По-точно, за любопитните
  • С две долни черти (self.__balance) Python преименува полето на _Account__balance. Отвън account.__balance вече го няма, но account._Account__balance пак стига до него. Това пази от случайно съвпадение на имена в наследниците, а не от умишлено пипане. Обикновено стига една черта.
  • Договорката важи и за методи: _check() е метод само за самия клас.

Идеята

Да четеш, без да пипаш

Отвън вече не бива и да прочетем баланса. Затова класът дава метод, който само го връща:

bank.py
class Account:
def __init__(self, owner, balance):
self._owner = owner
self._balance = balance
@property
def owner(self):
return self._owner
@property
def balance(self):
return self._balance
def deposit(self, amount):
if amount > 0:
self._balance += amount
def withdraw(self, amount):
if amount > 0 and amount <= self._balance:
self._balance -= amount
return True
return False
account = Account("Мария", 100)
account.withdraw(30)
account.deposit(50)
print(f"{account.owner}: {account.balance} €")
Изход
Мария: 120 €

balance е свойство (property). @property над метода значи, че отвън той се чете като поле: account.balance, без скоби. Вътре връща стойността на _balance. Това е getter-ът на Python. Сменя се балансът само през deposit и withdraw, а те проверяват.

Свойство без setter не се записва. Ред account.balance = -500 спира с:

Грешка
AttributeError: property 'balance' of 'Account' object has no setter

withdraw вече връща True или False: дали тегленето е минало. Така който тегли, разбира дали е успял.

account = Account("Мария", 100)
account.withdraw(500)
account.deposit(50)
print(account.balance)
Стъпка по стъпка, с думи
  1. _balance е вътрешно: по договорка отвън до полето не се пипа, а се минава през методите.
  2. Тегленето минава през проверката в метода: 500 е повече от 100. Отказано, балансът не се пипа.
  3. Внасянето също се проверява: 50 е положителна сума. Методът я добавя.
  4. Свойството balance дава навън копие на баланса. Самото поле _balance остава вътре.

Пробвай

Отказано

Какво ще отпечата програмата?

account = Account("Мария", 100)
print(account.withdraw(150))
account.deposit(-20)
print(account.balance)
Отговор

Идеята

Setter: промяна с проверка

Понякога полето трябва да може да се сменя отвън, но не с каквато и да е стойност. Възрастта на животно може да се промени, но не може да е отрицателна:

class Animal:
def __init__(self):
self._age = 0
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value >= 0:
self._age = value
cat = Animal()
cat.age = 4
cat.age = -3
print(cat.age)
Изход
4

@age.setter прави setter на свойството age: метод със същото име, който се вика при cat.age = -3. value е стойността отдясно на =, тук -3. Проверката не минава и възрастта остава 4. Отвън записът изглежда като в обикновено поле, но минава през проверката.

Идеята

Кое поле да има setter?

Не всяко поле има нужда от setter, а някои нямат нужда и от getter. Помисли какво трябва да може да се прави с продукта отвън:

  • името и цената се задават при създаването и после само се четат: само getter;
  • бройката в количката се сменя, но поне 1: getter и setter с проверка.
class Product:
def __init__(self, name, price):
self._name = name
self._price = price
self._quantity = 1
@property
def name(self):
return self._name
@property
def price(self):
return self._price
@property
def quantity(self):
return self._quantity
@quantity.setter
def quantity(self, value):
if value >= 1:
self._quantity = value
coffee = Product("Кафе", 7.50)
coffee.quantity = 3
coffee.quantity = 0
print(f"{coffee.name} x{coffee.quantity}")
Изход
Кафе x3

Правилото е: започни с _ за всичко и отваряй само каквото трябва. Setter, който записва всичко без проверка, връща проблема от първата карта.

Пробвай

Кое липсва?

Балансът е вътрешен за класа. Как се казва полето по договорка?
class Account:
    def __init__(self, balance):
        self. = balance

Пробвай

Намери грешката

Полето е _balance, а балансът пак става -500:

account = Account()
account.balance = 100
account.withdraw(30)
account.balance = -500
print(account.balance)
Изход
-500

Кой ред в класа пробива защитата?

Избери реда с грешката.

Упражнение

Скрити данни

Реши задачите в редактора си. Всички полета са с долна черта.

1. Човек. Person с име и възраст, конструктор, свойства name и age само за четене и introduce().

Изход
Мария е на 34 г.
Здравей, аз съм Мария и съм на 34 г.

2. Сметка без минус. Account със собственик и баланс. Внася се само положителна сума. Тегленето връща дали е минало и не пуска баланса под 0. Балансът се чете, но не се записва отвън.

Програмата
account = Account("Петър", 50)
account.deposit(25)
account.deposit(-10)
print(account.withdraw(100))
print(account.withdraw(40))
print(f"Баланс: {account.balance} €")
Изход
False
True
Баланс: 35 €

3. Животно. Animal с вид и възраст. Видът се задава в конструктора и само се чете. Възрастта има setter (@age.setter), който отказва отрицателно число и печата съобщение. Конструкторът също минава през тази проверка: в __init__ пиши self.age = age, без долна черта.

Програмата
cat = Animal("котка", 3)
cat.age = 4
cat.age = -1
print(f"{cat.species}, {cat.age} г.")
Изход
Невалидна възраст: -1
котка, 4 г.

Още задачи

  • Оценка. Клас Grade с име на предмет и оценка от 2 до 6. Setter-ът отказва всичко извън тези граници. Пробвай с 5, 7 и 1.
  • Продукт с отстъпка. Към Product от урока добави метод за отстъпка в проценти, който приема само от 1 до 90%. Цената няма setter.

Тест

Провери се

1. Полето е _balance. Може ли код извън класа да го промени?
2. Защо сметката няма setter за баланса?
3. В @age.setter def age(self, value): какво е value?
4. Какво значи капсулация?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·