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

Интерфейси като абстрактни класове

Проблем

Заплати и фактури

В края на месеца фирмата плаща две неща: заплатите на служителите и фактурите на доставчиците. Искаш един списък и един цикъл: на кого, колко, и накрая общата сума.

Фактурата не е служител. Да я накараш да наследи Employee е грешно. Изречението „фактурата е служител“ не е вярно, а тя ще получи и име, и заплата, и повишение.

Общ родител Payment с полета и код също не е верният отговор: служителите и фактурите нямат общи данни и общ код. Общо им е само, че могат да бъдат платени.

Идеята

Интерфейс: договор „мога да“

Интерфейсът е списък с методи без тяло: какво трябва да може класът, но не и как. Python няма отделна дума за него. Договорът се пише като абстрактен клас, в който всички методи са абстрактни:

from abc import ABC, abstractmethod
class Payable(ABC):
@abstractmethod
def amount_due(self):
pass
@abstractmethod
def payee(self):
pass
Клас, който приеме договора, реализира интерфейса и пише всеки негов метод:
class Invoice(Payable):
def __init__(self, supplier, amount):
self._supplier = supplier
self._amount = amount
def amount_due(self):
return self._amount
def payee(self):
return self._supplier
power = Invoice("Доставчик на ток", 340)
print(f"{power.payee()}: {power.amount_due()} €")
Изход
Доставчик на ток: 340 €

Payable стои в скобите като родител. Python не различава родител с код от родител-договор: разликата е, че Payable няма полета и няма тела на методите. Проверката е като при абстрактния клас: от Payable не се правят обекти. Payable() спира с

TypeError: Can't instantiate abstract class Payable without an implementation for abstract methods 'amount_due', 'payee'

Виж го

Един цикъл за всичко, което се плаща

Служителят също приема договора. Неговата сума е месечната заплата:

class Employee(Payable):
# ... _name, _salary, __init__, monthly_pay()
def amount_due(self):
return self.monthly_pay()
def payee(self):
return self._name

Сега и служителите, и фактурите са Payable: и двата класа го имат за родител. Списъкът няма тип, но всичко в него изпълнява договора, затова цикълът може да вика amount_due() и payee() на всеки.

payments = [
Employee("Иван", 2000),
Manager("Мария", 3000, 500),
Invoice("Доставчик на ток", 340),
Invoice("Наемодател", 1800),
]
total = 0
for p in payments:
print(f"{p.payee()}: {p.amount_due()} €")
total += p.amount_due()
print(f"Общо: {total} €")
Изход
Иван: 2000 €
Мария: 3500 €
Доставчик на ток: 340 €
Наемодател: 1800 €
Общо: 7640 €

Мениджърът не пише нищо ново. Той наследява от Employee и договора, и amount_due(). А amount_due() вика monthly_pay(), който мениджърът е презаписал. Затова Мария получава 3500 €.

payments = [
Employee("Иван", 2000),
Manager("Мария", 3000, 500),
Invoice("Доставчик на ток", 340),
]
total = 0
for p in payments:
total += p.amount_due()
print(f"Общо: {total} €")
Стъпка по стъпка, с думи
  1. Списъкът пази три обекта от три класа. Всички са Payable, а стрелките стигат само до договора им.
  2. total започва от 0.
  3. p сочи служителя: amount_due() връща заплатата, 2000.
  4. Мениджърът е служител, затова и той е платим. Заплата плюс бонус: 3500.
  5. Фактурата не е служител, но изпълнява същия договор: 340.
  6. Цикълът не знае класовете. Знае само, че всеки може да бъде платен.

Пробвай

Каква е сумата?

Какво ще отпечата програмата? Класовете са от горната карта.

p = Manager("Мария", 3000, 500)
print(p.amount_due())
Отговор

Идеята

Един родител, много договори

Счетоводството иска фактурите и в CSV формат: по един ред „доставчик;сума“. Това е втори договор. В Python клас може да има колкото родители иска, разделени със запетая, така че договорите са просто още родители:

class Exportable(ABC):
@abstractmethod
def to_csv(self):
pass
class Invoice(Payable, Exportable):
# ... както преди
def to_csv(self):
return f"{self._supplier};{self._amount}"
for_accounting = [Invoice("Доставчик на ток", 340), Invoice("Наемодател", 1800)]
for e in for_accounting:
print(e.to_csv())
Изход
Доставчик на ток;340
Наемодател;1800

Ако класът има и родител, и договори, родителят е пръв:

class Manager(Employee, Exportable):

Мениджърът тогава пише и to_csv(), и е едновременно Employee, Payable и Exportable. Редът в скобите има значение: когато два родителя имат метод с едно и също име, печели първият.

Родителите могат да са много, но тези с код и данни е добре да са един. Останалите са договори: само обещания, без полета.

Пробвай

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

Invoice("Доставчик на ток", 340) спира с TypeError. На кой ред е грешката?

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

Пробвай

Кое липсва?

class Invoice():
    # ...

Идеята

Абстрактен клас или интерфейс?

В Python и двете са абстрактни класове: договорът е абстрактен клас без полета и без код. Разликата не е в езика, а в това какво казват:

Абстрактен клас с код Договор
Казва „X е Y“ „X може да“
Колко на клас колкото искаш, но е добре един колкото искаш
Полета и __init__ да не
Общ код да почти не
  • Абстрактен клас, когато наследниците са един вид и имат общ код и данни: фигурите имат име и описание, служителите имат име и заплата.
  • Договор, когато класовете са различни, а им трябва обща способност: служител и фактура „могат да бъдат платени“, птица и самолет „могат да летят“.
Често се ползват заедно: Employee е родител на мениджъра и стажанта и същевременно реализира Payable.
По-точно, за любопитните
  • Договорът може да има и метод по подразбиране: обикновен метод с тяло до абстрактните. Класовете го получават наготово, а могат и да го презапишат. Така в договора се добавя нов метод, без да се чупят класовете, които вече го изпълняват:
class Flyable(ABC):
@abstractmethod
def fly(self):
pass
def glide(self):
print("Плъзга се във въздуха")
class Bird(Flyable):
def fly(self):
print("Птицата маха с крила")
class Plane(Flyable):
def fly(self):
print("Самолетът излита")
flyers = [Bird(), Plane()]
for f in flyers:
f.fly()
f.glide()
Изход
Птицата маха с крила
Плъзга се във въздуха
Самолетът излита
Плъзга се във въздуха
  • Има и втори начин за договор: typing.Protocol. Класът тогава не пише договора в скобите. Стига да има методите, той пасва, както в JavaScript. Самият Python протокола не го проверява: билетът без amount_due() минава като Payable. Проверява го отделен инструмент за типове, преди програмата да тръгне. С @runtime_checkable и isinstance (урокът „Какъв е всъщност?“) може да се попита дали обектът има методите:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Payable(Protocol):
def amount_due(self): ...
def payee(self): ...
class Invoice:
def __init__(self, supplier, amount):
self._supplier = supplier
self._amount = amount
def amount_due(self):
return self._amount
def payee(self):
return self._supplier
class Ticket:
def payee(self):
return "Кино"
p: Payable = Ticket()
print(isinstance(Invoice("Наемодател", 1800), Payable))
print(isinstance(p, Payable))
Изход
True
False

Упражнение

Договори

Реши задачите в редактора си. В Python всеки „интерфейс“ е абстрактен клас (ABC) само с абстрактни методи.

1. Клаксон. Интерфейс Honkable с метод honk(). SimpleCar от модул „Класове“ го реализира.

Изход
Бип-бип!

2. Подробности. Интерфейс Printable с метод print_details(). Реализирай го в Person, а Student (наследник на Person) да го презапише и да добави успеха. Сложи двамата в един списък.

Изход
Име: Георги, възраст: 45
Име: Елена, възраст: 19
Среден успех: 5.5

3. Танц. Интерфейс Danceable с метод dance(). DanceMove (име и продължителност) го реализира.

Изход
Танцувам салса 30 секунди

4. Диджей. Клас Song със заглавие. Интерфейс Playable с метод play(). DJ го реализира и има метод play_song(song), който запомня песента и я пуска. Ако още няма песен, play() казва това.

Изход
Няма избрана песен
Пускам 'Bohemian Rhapsody'

5. Езерото. Интерфейси Swimmable с swim() и Flyable с fly(). Fish плува. Duck и плува, и лети.

Изход
Рибата плува.
Патицата плува.
Патицата лети.

Още задачи

  • Отстъпка. Интерфейс Discountable с метод, който връща цената след отстъпка. Реализирай го в Product (10% отстъпка) и в Subscription (25% отстъпка за студенти). Сметни сумата на количка с продукт за 50 € и студентски абонамент за 12 €: 45 + 9 = 54 €.
  • CSV за всички. Накарай и Employee да реализира Exportable, с ред „име;месечна заплата“. Сложи служители и фактури в един списък.

Тест

Провери се

1. Колко договора (абстрактни класа) може да изпълнява един клас?
2. Payable наследява ABC и има два абстрактни метода. Какво става при Payable()?
3. Invoice(Payable) пише amount_du() вместо amount_due(). Кога ще разбереш?
4. Кой проверява договор, записан с typing.Protocol?
5. Служители и фактури трябва да се плащат в един цикъл. Те нямат общ код. Какво е по-подходящо?

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