Interfaces as abstract classes
Problem
Salaries and invoices
At the end of the month the company pays two kinds of things: the employees' salaries and the suppliers' invoices. You want one list and one loop: who gets paid, how much, and the total at the end.
An invoice is not an employee. Making it inherit from Employee is wrong. The sentence "an invoice is an employee" is not true, and the invoice would also get a name, a salary and a raise.
A common parent Payment with fields and code is not the right answer either: employees and invoices have no shared data and no shared code. All they have in common is that they can be paid.
The idea
An interface: a "can do" contract
An interface is a list of methods without bodies: what a class must be able to do, but not how. Python has no separate word for it. You write the contract as an abstract class in which all methods are abstract:
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("Power company", 340)print(f"{power.payee()}: €{power.amount_due()}")Power company: €340Payable sits in the parentheses like a parent. Python does not tell a parent with code apart from a contract parent: the difference is that Payable has no fields and no method bodies. The check works like it does for an abstract class: you cannot create objects from Payable. Payable() stops with
TypeError: Can't instantiate abstract class Payable without an implementation for abstract methods 'amount_due', 'payee'See it
One loop for everything that gets paid
The employee accepts the contract too. Its amount is the monthly salary:
class Employee(Payable): # ... _name, _salary, __init__, monthly_pay()
def amount_due(self): return self.monthly_pay()
def payee(self): return self._nameNow both the employees and the invoices are Payable: both classes have it as a parent. The list has no type, but everything in it keeps the contract, so the loop can call amount_due() and payee() on each one.
payments = [ Employee("Ivan", 2000), Manager("Maria", 3000, 500), Invoice("Power company", 340), Invoice("Landlord", 1800),]total = 0for p in payments: print(f"{p.payee()}: €{p.amount_due()}") total += p.amount_due()print(f"Total: €{total}")Ivan: €2000Maria: €3500Power company: €340Landlord: €1800Total: €7640The manager writes nothing new. From Employee it inherits both the contract and amount_due(). And amount_due() calls monthly_pay(), which the manager has overridden. That is why Maria gets €3500.
payments = [ Employee("Ivan", 2000), Manager("Maria", 3000, 500), Invoice("Power company", 340),]total = 0for p in payments: total += p.amount_due()print(f"Total: €{total}")Step by step, in words
- The list holds three objects of three classes. They are all Payable, and the arrows reach only their contract.
- total starts at 0.
- p points to the employee: amount_due() returns the salary, 2000.
- The manager is an employee, so the manager can be paid too. Salary plus bonus: 3500.
- The invoice is not an employee, but it keeps the same contract: 340.
- The loop does not know the classes. It only knows that each one can be paid.
Try it
What is the amount?
What will the program print? The classes are from the card above.
p = Manager("Maria", 3000, 500)print(p.amount_due())Manager is an Employee, and Employee implements the contract. So the manager implements it too, without writing it. The call goes to the manager's overridden method: 3000 + 500.
The idea
One parent, many contracts
Accounting also wants the invoices in CSV format: one "supplier;amount" line each. That is a second contract. In Python a class can have as many parents as it wants, separated by commas, so contracts are just more parents:
class Exportable(ABC): @abstractmethod def to_csv(self): pass
class Invoice(Payable, Exportable): # ... as before
def to_csv(self): return f"{self._supplier};{self._amount}"for_accounting = [Invoice("Power company", 340), Invoice("Landlord", 1800)]for e in for_accounting: print(e.to_csv())Power company;340Landlord;1800If the class has both a parent and contracts, the parent comes first:
class Manager(Employee, Exportable):The manager then writes to_csv() as well, and is an Employee, a Payable and an Exportable at the same time. The order in the parentheses matters: when two parents have a method with the same name, the first one wins.
There can be many parents, but it is best to have only one with code and data. The rest are contracts: only promises, no fields.
Try it
Find the bug
Invoice("Power company", 340) stops with TypeError. Which line has the bug?Pick the line with the bug.
amount_du instead of amount_due. To Python this is just another method, and the promised one is still not written. So you cannot create an invoice: TypeError: Can't instantiate abstract class Invoice without an implementation for abstract method 'amount_due'.Try it
What is missing?
class Invoice():
# ...Payable: the contract sits in the parentheses like a parent. Without it the invoice still has the methods, but it is not a Payable, and Python does not check that all of them are written.The idea
Abstract class or interface?
In Python both are abstract classes: a contract is an abstract class with no fields and no code. The difference is not in the language but in what they say:
| Abstract class with code | Contract | |
|---|---|---|
| Says | "X is a Y" | "X can" |
| How many per class | as many as you want, but one is best | as many as you want |
Fields and __init__ |
yes | no |
| Shared code | yes | hardly any |
- An abstract class, when the classes below it are one kind of thing and share code and data: shapes have a name and a description, employees have a name and a salary.
- A contract, when the classes are different but need a common ability: an employee and an invoice "can be paid", a bird and a plane "can fly".
Employee is the parent of the manager and the intern, and at the same time it implements Payable.More precisely, if you are curious
- A contract can also have a default method: an ordinary method with a body next to the abstract ones. Classes get it ready-made, and they can also override it. That way you can add a new method to the contract without breaking the classes that already fulfill it:
class Flyable(ABC): @abstractmethod def fly(self): pass
def glide(self): print("Gliding through the air")
class Bird(Flyable): def fly(self): print("The bird flaps its wings")
class Plane(Flyable): def fly(self): print("The plane takes off")
flyers = [Bird(), Plane()]for f in flyers: f.fly() f.glide()The bird flaps its wingsGliding through the airThe plane takes offGliding through the air- There is a second way to write a contract:
typing.Protocol. The class then does not write the contract in its parentheses. As long as it has the methods, it fits, just like in JavaScript. Python itself does not check the protocol: the ticket withoutamount_due()passes as aPayable. A separate type checker checks it before the program runs. With@runtime_checkableandisinstance(the lesson "What is it really?") you can ask whether the object has the methods:
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 "Cinema"
p: Payable = Ticket()print(isinstance(Invoice("Landlord", 1800), Payable))print(isinstance(p, Payable))TrueFalseExercise
Contracts
Solve the tasks in your editor. In Python each "interface" is an abstract class (ABC) with only abstract methods.
1. Horn. An interface Honkable with a method honk(). SimpleCar from the module "Classes and objects" implements it.
Beep beep!2. Details. An interface Printable with a method print_details(). Implement it in Person, and have Student (a child of Person) override it and add the grade. Put both of them in one list.
Name: Georgi, age: 45Name: Elena, age: 19Average grade: 5.53. Dance. An interface Danceable with a method dance(). DanceMove (a name and a duration) implements it.
Dancing salsa for 30 seconds4. DJ. A class Song with a title. An interface Playable with a method play(). DJ implements it and has a method play_song(song) that remembers the song and plays it. If there is no song yet, play() says so.
No song selectedPlaying 'Bohemian Rhapsody'5. The pond. Interfaces Swimmable with swim() and Flyable with fly(). Fish swims. Duck both swims and flies.
The fish swims.The duck swims.The duck flies.More tasks
- Discount. An interface Discountable with a method that returns the price after the discount. Implement it in
Product(10% off) and inSubscription(25% off for students). Work out the total of a cart with a €50 product and a €12 student subscription: €45 + €9 = €54. - CSV for everyone. Make
Employeeimplement Exportable too, with a "name;monthly salary" line. Put employees and invoices in one list.
Test