Polymorphism
Problem
Pay with an if for each kind
The company has regular employees, managers with a bonus and part-time interns. Someone made one Employee class with a "role" field, and the pay is worked out like this:
def monthly_pay(e): if e.role == "manager": return e.salary + e.bonus elif e.role == "intern": return e.salary // 2 return e.salaryIt works. But then a new kind of employee comes along: a contractor. You need a new elif here, and in the function for taxes, and in the function for the payslip, and everywhere anyone asked about the role. And every employee carries a bonus field, even though only managers get a bonus.
The code for the manager is scattered all over the program instead of being in the manager.
The idea
Each class decides for itself
With inheritance from the last lesson, Manager and Intern are separate classes. Let each one say what its monthly pay is. The parent has a general version, and the child writes its own version of the same method:
class Employee: # ... __init__, name and salary
def monthly_pay(self): return self._salary
class Manager(Employee): # ... _bonus and __init__
def monthly_pay(self): return self.salary + self._bonusivan = Employee("Ivan", 2000)maria = Manager("Maria", 3000, 500)print(ivan.monthly_pay())print(maria.monthly_pay())20003500This is called overriding a method. The child writes a method with the same name. For objects of the child class, its version replaces the parent's.
You need nothing more: there is no keyword and no check. Any method can be overridden. Python looks only at the name, not at the parameters. That is handy, but it means nothing stops a mistake in the name or in the parameters. In a moment you will see what happens then.
More precisely, if you are curious
- Since Python 3.12 there is
@overridefrom thetypingmodule. You put it above the overriding method. Python itself does not check it: the program runs the same way with it and without it. A tool like mypy checks it, by reading the code before you run it. - There is also no way to forbid child classes from changing a method.
@finalfromtypingis only a note for such tools.
Try it
What changed?
The intern overrides only the method for the monthly pay:
What will the program print?
class Intern(Employee): def monthly_pay(self): return self.salary // 2
ana = Intern("Ana", 1600)print(ana.monthly_pay())print(ana.salary)The intern's monthly_pay() returns half the salary. The salary itself does not change, and salary is inherited from Employee as it is.
The idea
One list for all
A manager is an employee. So employees, managers and interns can sit in one list and be handled in one loop:
staff = [ Employee("Ivan", 2000), Manager("Maria", 3000, 500), Intern("Ana", 1600),]for e in staff: print(f"{e.name}: €{e.monthly_pay()}")Ivan: €2000Maria: €3500Ana: €800The loop calls the same method, e.monthly_pay(), and the result is different. The version that runs is the object's, not the one of the variable's type. This is called polymorphism: Greek for "many forms". One call, and each object answers in its own way.
Now a new kind of employee is a new class with its own monthly_pay(). The loop and the rest of the program stay as they are. There is no if on the role and no bonus field in employees who get no bonus.
The variable e has no type, so through it you can write anything you like, e.bonus too. If the object is a manager, it works. If it is a regular employee, the program stops on that line: AttributeError: 'Employee' object has no attribute 'bonus'. Python does not check ahead of time. How to ask objects what they can do is in the lesson "What is it really?".
See it
Which method runs
staff = [ Employee("Ivan", 2000), Manager("Maria", 3000, 500), Intern("Ana", 1600),]for e in staff: print(f"{e.name}: €{e.monthly_pay()}")Step by step, in words
- The list holds objects of three classes. Each slot holds an arrow.
- e points to Ivan, a plain Employee: Employee's monthly_pay runs.
- e points to Maria, a Manager: her own method runs, salary plus bonus.
- e points to Ana, an Intern: the intern gets half the salary.
- The same line, three different methods. The object e points to decides.
The list holds three arrows. Each one points to an object of a different class. On each round e points to the next object, and the call goes to that object's method: first in Employee, then in Manager, then in Intern.
The choice is made while the program runs. On each round Python looks for monthly_pay in the object's class first, then in its parent. Nobody checks ahead of time that every object has such a method. If one does not, the program stops on its round.
Try it
Find the bug
The program is the same, but the intern gets the full salary:
Ivan: €2000Maria: €3500Ana: €1600Here is the Intern class. Which line has the bug?
Pick the line with the bug.
monthlypay, without the underscore. That is a new method, not an override of monthly_pay. So the parent's version is called. Python says nothing: to Python it is just one more method. @override from typing above the method would not stop the program, but a tool like mypy would catch it.The idea
Add to the parent, do not rewrite it
Sometimes the child does not want to replace the method completely, only to add something to it. A manager is described like an employee, plus the bonus. You call the parent's version with super().__str__():
def __str__(self): return f"{super().__str__()}, bonus €{self._bonus}"print(Employee("Ivan", 2000))print(Manager("Maria", 3000, 500))Ivan (€2000)Maria (€3000), bonus €500If Employee changes how it describes itself, the manager follows on its own.
This is also the __str__ from the "Classes and objects" module. There you were already overriding a method: the version in object.
More precisely, if you are curious
- You can call
super().method()from any method of the child, not only from the overriding one.
- Python does not compare the parameters. If you write
def monthly_pay(self, extra):inManager, it is still an override, and the loop that callse.monthly_pay()stops at the manager:TypeError: Manager.monthly_pay() missing 1 required positional argument: 'extra'. So the overriding method takes the same parameters as the parent's.
Try it
What is missing?
The intern is described like an employee plus the word "intern". What goes in the blank?
class Intern(Employee):
def __str__(self):
return f"{.__str__()}, intern"super().__str__() gives "Ana (€1600)", and the child adds ", intern". With self.__str__() instead, the method would call itself, forever, until Python stopped the program with RecursionError.
Exercise
Each in its own way
Solve the tasks in your editor. Each task has a list of objects of several classes and one loop.
1. Animal shelter. An Animal class with a name and a make_sound() method that prints "some sound". Dog and Cat override it. Put one of each of the three in a list:
Guest: some soundRex: Woof!Mitzi: Meow, meow!2. The student introduces themselves. Student from the last lesson already has a grade. Override introduce() so that it says the grade too. Use super().introduce(); do not copy the line from Person.
I am Georgi, 45 years old.I am Elena, 19 years old.Average grade: 5.53. Party. A Performance class with a title and a start() method. DanceMove, BreakDanceMove (inherits from DanceMove, adds difficulty) and Song override it. A Party class holds a list of performances and has a start_party() method that starts everything in order and at the end says the party has started.
Dancing moonwalkDancing windmill difficulty 8 out of 10Singing 'Bohemian Rhapsody'Starting raffleThe party has started!More tasks
- Contractor. Add a
Contractorto the employees, with hours and a rate per hour. Their monthly pay is hours × rate. Work out the payroll for Ivan, Maria, Ana and Peter (60 hours at €25):
Payroll: €7800- Payslip. Give
Employeeapayslip()method that returns the text "name: €pay" with the monthly pay. Write it only inEmployee. Why does it work correctly for the manager and for the intern too?
Test