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

Поведение

Проблем

Всеки тегли по своему

Банката има клас Account със собственик и баланс. Парите се теглят на две места в програмата. Банкоматът проверява дали има достатъчно пари. Плащането с карта е писано от друг човек и не проверява:

account = Account("Мария", 100)
# Банкоматът проверява дали има достатъчно пари.
if account.balance >= 70:
account.balance -= 70
# Плащането с карта е писано от друг човек и проверка няма.
account.balance -= 50
print(account.balance)
Изход
-20

Сметката е на минус. Правилото „не можеш да изтеглиш повече, отколкото имаш“ е важно, а е написано (или забравено) поотделно на всяко място, където се тегли.

Идеята

Методите живеят при данните

Логиката за теглене е част от сметката, затова я сложи в класа Account. Функция, написана в класа, е метод на обекта:

bank.py
class Account:
def __init__(self, owner, balance):
self.owner = owner
self.balance = balance
def deposit(self, amount):
self.balance += amount
def withdraw(self, amount):
if amount <= self.balance:
self.balance -= amount
else:
print(f"Няма достатъчно пари: {self.owner}")
account = Account("Мария", 100)
account.withdraw(70)
account.withdraw(50)
account.deposit(20)
print(account.balance)
Изход
Няма достатъчно пари: Мария
50
  • Методът е def, отместен в класа. Първият му параметър е self, както при __init__.
  • Вика се през обект и точка: account.withdraw(70), както полетата. Python слага account в self сам, затова подаваш само сумата.
  • Вътре self.balance е балансът на тази сметка, на обекта пред точката. self. е задължително: без него balance не е полето.
  • Проверката е написана веднъж. Който тегли през метода, минава през нея.

maria = Account("Мария", 100)
peter = Account("Петър", 40)
maria.withdraw(70)
peter.withdraw(70)
print(maria.balance, peter.balance)
Стъпка по стъпка, с думи
  1. Две сметки, два обекта. Всеки има свой баланс.
  2. maria.withdraw(70): в self отива стрелка към обекта пред точката.
  3. В метода self.balance е балансът на Мария: 100 - 70 = 30.
  4. Същият метод, друг обект: сега self сочи Петър.
  5. 70 е повече от 40: методът печата отказ и балансът на Петър не се пипа.
  6. Кодът на метода е един. Полетата са на обекта, през който го викаш.

Плащането с карта вече не може да „забрави“ проверката, ако тегли с withdraw. Може обаче още да пише направо в полето. Как да го спрем, ще видим в урока „Капсулация“.

Идеята

Метод или функция

Функциите от модул „Функции“ стоят извън класа. Те получават всичко като параметри и нямат свой обект. Методът работи с полетата на обекта, през който е извикан:

def apply_discount(price, percent):
return price * (100 - percent) / 100
price = apply_discount(10, 10)
account.withdraw(70)

Метод на обекта не можеш да извикаш през името на класа. Не е ясно от чия сметка да тегли:

Account.withdraw(70)
Грешка
TypeError: Account.withdraw() missing 1 required positional argument: 'amount'

70 отиде в self, а за amount не остана нищо. Всъщност account.withdraw(70) е съкратено от Account.withdraw(account, 70): обектът пред точката става първият аргумент. Методи на самия клас, без обект, също има. Ще ги видим в урока „Общото за всички: атрибути на класа“.

Пробвай

Чия сметка?

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

maria = Account("Мария", 100)
peter = Account("Петър", 40)
maria.deposit(30)
peter.withdraw(10)
print(maria.balance, peter.balance)
Отговор

Пробвай

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

account.withdraw(30) спира програмата с грешка. На кой ред е грешката?

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

Идеята

Методът може и да отговаря

Метод на обекта може да връща резултат, както функциите от модул „Функции“. Продуктът знае цената и бройката си, затова може сам да каже колко струва:

class Product:
# полетата и конструкторът, както преди
def total(self):
return self.price * self.quantity
cart = [
Product("Хляб", 1.20, 2),
Product("Мляко", 2.10, 1),
Product("Кафе", 7.50, 1),
]
amount = 0
for p in cart:
amount += p.total()
print(f"Общо: {amount} €")
Изход
Общо: 12.0 €

Сравни с първия урок от модула: там сумата се смяташе в кода под класа от полетата. Сега този код само пита всеки продукт. Ако утре се добави отстъпка за количество, променяш само total().

Идеята

Как изглежда обектът като текст?

Отпечаташ ли продукт направо, излиза нещо безполезно:

coffee = Product("Кафе", 7.50, 1)
print(coffee)
Изход
<__main__.Product object at 0x7f3a2c1b2d10>

Python печата името на класа и адреса на обекта в паметта, който е различен при всяко пускане. По него не разбираш нищо за продукта.

Класът може да каже как да се показва. Методът се казва __str__ и връща текст:

class Product:
# полетата и конструкторът, както преди
def total(self):
return self.price * self.quantity
def __str__(self):
return f"{self.name} x{self.quantity}: {self.total()} €"
coffee = Product("Кафе", 7.50, 1)
bread = Product("Хляб", 1.20, 2)
print(coffee)
print(f"В количката: {bread}")
Изход
Кафе x1: 7.5 €
В количката: Хляб x2: 2.4 €

Не го викаш сам. print(coffee) и f-низът f"В количката: {bread}" го викат вместо теб, когато трябва обектът да стане текст. Двете долни черти отпред и отзад значат точно това: методът е за Python, а той го вика сам в точния момент, както __init__ при създаването.

С + обаче се долепя само текст до текст. "В количката: " + bread спира с

Грешка
TypeError: can only concatenate str (not "Product") to str

Ползвай f-низ или "В количката: " + str(bread): str() също вика __str__.

По-точно, за любопитните

Освен __str__ има и __repr__: текстът за програмиста. Него ползва списък, когато го печаташ, затова без __repr__ print(cart) пак дава <__main__.Product object at …>. По договорка __repr__ изглежда като кода, който създава обекта:

class Product:
# полетата и конструкторът, както преди
def __repr__(self):
return f'Product("{self.name}", {self.price}, {self.quantity})'
cart = [Product("Кафе", 7.50, 1), Product("Хляб", 1.20, 2)]
print(cart)
Изход
[Product("Кафе", 7.5, 1), Product("Хляб", 1.2, 2)]

Пробвай

Книга в изречение

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

class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def __str__(self):
return f"{self.title} от {self.author}"
book = Book("Тютюн", "Димитър Димов")
print(f"Чета: {book}")
Отговор

Упражнение

Обекти, които правят нещо

Реши задачите в редактора си. Вземи класовете от миналите уроци и им добави методи.

  1. Person: метод introduce(), който печата името и възрастта.
  2. SimpleCar: метод accelerate(), който увеличава скоростта с 10. Сега кодът под класа не пипа скоростта направо.
  3. DanceMove: метод perform(), който „изпълнява“ стъпката.
  4. Book: метод read(), който печата „Прочитане на [заглавие] от [автор]“.

Създай по един обект от всеки клас и извикай методите му:

Изход
Здравей, аз съм Мария и съм на 34 г.
Скорост след ускоряване: 10
Изпълнявам moonwalk за 5 секунди
Прочитане на Под игото от Иван Вазов

Упражнение

Кола с гориво

Клас Car с марка, скорост, гориво (в литри) и изминати километри. Конструкторът получава марката и горивото, скоростта и километрите започват от 0.

  • accelerate() увеличава скоростта с 10.
  • refuel(liters) долива гориво, но резервоарът побира най-много 50 литра.
  • drive(distance) харчи по 6 литра на 100 км и добавя разстоянието към километрите. Ако горивото не стига, печата съобщение и колата не тръгва.
  • __str__() връща марката, километрите и горивото (горивото с един знак след точката: {self.fuel:.1f}).
Програмата
car = Car("Toyota", 20)
car.accelerate()
car.accelerate()
print(f"Скорост: {car.speed}")
car.drive(100)
print(car)
car.drive(300)
car.refuel(40)
print(car)
car.drive(300)
print(car)
Изход
Скорост: 20
Toyota: 100 км, 14.0 л
Няма достатъчно гориво за 300 км
Toyota: 100 км, 50.0 л
Toyota: 400 км, 32.0 л
Подсказка: distance * 6 / 100. В Python / винаги дава дробно число, затова за 30 км излиза 1,8 литра.

Още задачи

  • Отстъпка. Добави към Product метод apply_discount(percent), който намалява цената на продукта. Сравни го със функцията от модул „Функции“, която връщаше нова цена.
  • Сметка с история. Добави към Account брояч на операциите: всяко внасяне и успешно теглене го увеличава с 1. Метод __str__() връща собственика, баланса и броя на операциите.

Тест

Провери се

1. Какво прави maria.deposit(30)?
2. Какво вижда методът, което функцията извън класа не вижда?
3. Защо Account.withdraw(70) спира с TypeError?
4. Кога Python вика __str__ сам?

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