Капсулация
Проблем
Минус 500 €
В миналия урок сложихме проверката за теглене в метод. Само че полето е отворено и всеки може да пише в него направо:
account = Account("Мария", 100)account.withdraw(500)account.balance = -500print(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()е метод само за самия клас.
Идеята
Да четеш, без да пипаш
Отвън вече не бива и да прочетем баланса. Затова класът дава метод, който само го връща:
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 setterwithdraw вече връща True или False: дали тегленето е минало. Така който тегли, разбира дали е успял.
account = Account("Мария", 100)account.withdraw(500)account.deposit(50)print(account.balance)Стъпка по стъпка, с думи
- _balance е вътрешно: по договорка отвън до полето не се пипа, а се минава през методите.
- Тегленето минава през проверката в метода: 500 е повече от 100. Отказано, балансът не се пипа.
- Внасянето също се проверява: 50 е положителна сума. Методът я добавя.
- Свойството balance дава навън копие на баланса. Самото поле _balance остава вътре.
Пробвай
Отказано
Какво ще отпечата програмата?
account = Account("Мария", 100)print(account.withdraw(150))account.deposit(-20)print(account.balance)150 € е повече от баланса, затова withdraw връща False и не пипа баланса. Внасянето на -20 € също се отказва, защото сумата трябва да е положителна. Балансът остава 100.
Идеята
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 = 4cat.age = -3print(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 = 3coffee.quantity = 0print(f"{coffee.name} x{coffee.quantity}")Кафе x3Правилото е: започни с _ за всичко и отваряй само каквото трябва. Setter, който записва всичко без проверка, връща проблема от първата карта.
Пробвай
Кое липсва?
class Account:
def __init__(self, balance):
self. = balance_balance: долната черта казва „вътрешно за класа, отвън не се пипа“.Пробвай
Намери грешката
Полето е _balance, а балансът пак става -500:
account = Account()account.balance = 100account.withdraw(30)account.balance = -500print(account.balance)-500Кой ред в класа пробива защитата?
Избери реда с грешката.
Setter-ът под @balance.setter записва всяка стойност без проверка. Полето е вътрешно, но setter-ът го отваря отново. Махни го и задавай началния баланс в __init__. Сменя се само с deposit и withdraw.
Упражнение
Скрити данни
Реши задачите в редактора си. Всички полета са с долна черта.
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} €")FalseTrueБаланс: 35 €3. Животно. Animal с вид и възраст. Видът се задава в конструктора и само се чете. Възрастта има setter (@age.setter), който отказва отрицателно число и печата съобщение. Конструкторът също минава през тази проверка: в __init__ пиши self.age = age, без долна черта.
cat = Animal("котка", 3)cat.age = 4cat.age = -1print(f"{cat.species}, {cat.age} г.")Невалидна възраст: -1котка, 4 г.Още задачи
- Оценка. Клас
Gradeс име на предмет и оценка от 2 до 6. Setter-ът отказва всичко извън тези граници. Пробвай с 5, 7 и 1. - Продукт с отстъпка. Към
Productот урока добави метод за отстъпка в проценти, който приема само от 1 до 90%. Цената няма setter.
Тест