Интерфейси като абстрактни класове
Проблем
Заплати и фактури
В края на месеца фирмата плаща две неща: заплатите на служителите и фактурите на доставчиците. Искаш един списък и един цикъл: на кого, колко, и накрая общата сума.
Фактурата не е служител. Да я накараш да наследи Employee е грешно. Изречението „фактурата е служител“ не е вярно, а тя ще получи и име, и заплата, и повишение.
Общ родител Payment с полета и код също не е верният отговор: служителите и фактурите нямат общи данни и общ код. Общо им е само, че могат да бъдат платени.
Идеята
Интерфейс: договор „мога да“
Интерфейсът е списък с методи без тяло: какво трябва да може класът, но не и как. Python няма отделна дума за него. Договорът се пише като абстрактен клас, в който всички методи са абстрактни:
from abc import ABC, abstractmethod
class Payable(ABC): @abstractmethod def amount_due(self): pass
@abstractmethod def payee(self): passclass 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 = 0for 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 = 0for p in payments: total += p.amount_due()print(f"Общо: {total} €")Стъпка по стъпка, с думи
- Списъкът пази три обекта от три класа. Всички са Payable, а стрелките стигат само до договора им.
- total започва от 0.
- p сочи служителя: amount_due() връща заплатата, 2000.
- Мениджърът е служител, затова и той е платим. Заплата плюс бонус: 3500.
- Фактурата не е служител, но изпълнява същия договор: 340.
- Цикълът не знае класовете. Знае само, че всеки може да бъде платен.
Пробвай
Каква е сумата?
Какво ще отпечата програмата? Класовете са от горната карта.
p = Manager("Мария", 3000, 500)print(p.amount_due())Manager е Employee, а Employee реализира договора. Затова и мениджърът го реализира, без да го пише. Извикването отива в презаписания метод на мениджъра: 3000 + 500.
Идеята
Един родител, много договори
Счетоводството иска фактурите и в 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. На кой ред е грешката?Избери реда с грешката.
amount_du вместо amount_due. За Python това е просто друг метод, а обещаният остава ненаписан. Затова фактура не може да се създаде: TypeError: Can't instantiate abstract class Invoice without an implementation for abstract method 'amount_due'.Пробвай
Кое липсва?
class Invoice():
# ...Payable: договорът стои в скобите като родител. Без него фактурата пак има методите, но не е Payable и Python не проверява, че са написани всички.Идеята
Абстрактен клас или интерфейс?
В 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_checkableclass 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))TrueFalseУпражнение
Договори
Реши задачите в редактора си. В Python всеки „интерфейс“ е абстрактен клас (ABC) само с абстрактни методи.
1. Клаксон. Интерфейс Honkable с метод honk(). SimpleCar от модул „Класове“ го реализира.
Бип-бип!2. Подробности. Интерфейс Printable с метод print_details(). Реализирай го в Person, а Student (наследник на Person) да го презапише и да добави успеха. Сложи двамата в един списък.
Име: Георги, възраст: 45Име: Елена, възраст: 19Среден успех: 5.53. Танц. Интерфейс 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, с ред „име;месечна заплата“. Сложи служители и фактури в един списък.
Тест