Inheritance
Problem
Two copies of the same code
The payroll program has an Employee class with a name, a salary and a method for a raise. Then you needed a manager too, who also gets a bonus. The fastest way was to copy Employee into a new class Manager and add the bonus.
A month later someone enters a raise of -10% instead of 10%. So you add a check to Employee. You forget it in Manager (the method is raise_salary, because raise is a reserved word in Python):
# in Employeedef raise_salary(self, percent): if percent > 0: self._salary += self._salary * percent // 100
# in Manager: a copy from before the checkdef raise_salary(self, percent): self._salary += self._salary * percent // 100ivan = Employee("Ivan", 2000)maria = Manager("Maria", 3000, 500)ivan.raise_salary(-10)maria.raise_salary(-10)print(ivan)print(maria)Ivan: €2000Maria: €2700Ivan's salary is protected, but Maria's dropped by €300. The two classes are 90% the same, yet every fix has to be made twice.
The idea
The shared part goes in the parent
A manager is an employee, just with something extra. Say it in code: class Manager(Employee):. In Manager you write only what is new, the bonus:
class Manager(Employee): def __init__(self, name, salary, bonus): super().__init__(name, salary) self._bonus = bonus
@property def bonus(self): return self._bonus
maria = Manager("Maria", 3000, 500)maria.raise_salary(-10)maria.raise_salary(10)print(maria)print(f"Bonus: €{maria.bonus}")Maria: €3300Bonus: €500Manager now has everything Employee has: a name, a salary, raise_salary() with the check, __str__(). That is why the -10% raise did not go through. The check is written once and works for both classes.
This is called inheritance. Employee is the parent (base class), and Manager is the child. The child takes everything from the parent and adds its own.
To check whether inheritance makes sense, use the sentence "X is a Y". "A manager is an employee" is true. "An invoice is an employee" is not, so the invoice does not inherit from Employee, even if it has an amount and a name.
Try it
What does the intern get?
Intern has nothing of its own: the body is just pass.What will the program print?
class Intern(Employee): pass
ana = Intern("Ana", 1200)ana.raise_salary(5)print(ana)raise_salary() and __str__() are inherited from Employee. 5% of 1200 is 60, and 1200 + 60 = 1260. __init__ is inherited too: that is why Intern("Ana", 1200) passes the name and the salary straight to Employee.
The idea
The parent comes first
Manager has its own __init__, because it also has a bonus. But the name and the salary are set up by the __init__ of Employee. Who calls it?
Python calls only one __init__: the one of the object's class. You have to call the parent one yourself:
def __init__(self, name, salary, bonus): super().__init__(name, salary) self._bonus = bonussuper() is "the parent of this class", and super().__init__(name, salary) calls its __init__ with these arguments. It sets up _name and _salary, and then Manager sets up _bonus. Python allows this line anywhere in __init__, but write it first: that way the parent's part is ready before the child uses it.
If you forget it, Python says nothing until the field is needed. print(maria) stops with AttributeError: 'Manager' object has no attribute '_name': the field was simply never created.
There is one object. It holds both the parent's part and the child's part, and each part is set up by its own constructor.
def __init__(self, name, salary, bonus): super().__init__(name, salary) self._bonus = bonus
maria = Manager("Maria", 3000, 500)print(f"{maria}, bonus €{maria.bonus}")Step by step, in words
- Manager(...) creates one object. For now it is empty: each field appears when __init__ gives it a value.
- __init__ of Manager starts with the three arguments.
- The parent comes first: super().__init__(name, salary) calls __init__ of Employee. Python does not call it for you.
- __init__ of Employee sets up its part: _name and _salary.
- Only then does the body of Manager set up the bonus.
- There is one object, and each part is set up by its own constructor.
More precisely, if you are curious
__init__is inherited like any other method. That is whyIntern, with no__init__of its own, takes the name and the salary ofEmployee. You write your own__init__when the child has something new, and then it replaces the parent's.- In older code you will see
super(Manager, self).__init__(...)orEmployee.__init__(self, ...). They do the same thing.super()with nothing in the parentheses is how you write it today.
Try it
Which constructor runs first?
Here the child does not call the parent explicitly:
What will the program print?
class Account: def __init__(self): print("Account opened")
class SavingsAccount(Account): def __init__(self): print("Interest: 3%")
savings = SavingsAccount()__init__ by itself. The child has its own __init__, and that is the only one that runs. If you want the account to be opened too, the first line has to be super().__init__().Try it
Find the bug
Maria should have a salary of €3000 and a bonus of €500. But the program prints:
Maria: €500Which line is the bug on?
Pick the line with the bug.
The parent gets the bonus, not the salary. Arguments are matched to parameters by position, so Employee gets 500 as the salary. The fix is super().__init__(name, salary).
The idea
The underscore: for the class and its children
At the end of the year, the manager's bonus is added to the salary. The method is in Manager, and the salary is _salary in Employee. The salary property only reads it, and here you have to change it.
In Java and C# there is a third access level for this, protected: the class and its children can see it, and everyone else cannot. Python has neither private nor protected. The underscore is a convention for exactly this: "internal, for the class and its children". That is why Manager writes straight to self._salary, and Employee does not change:
class Employee: def __init__(self, name, salary): self._name = name self._salary = salary # ... the rest is as before
class Manager(Employee): # ... _bonus and __init__
def merge_bonus(self): self._salary += self._bonus self._bonus = 0maria = Manager("Maria", 3000, 500)maria.merge_bonus()print(maria)print(f"Bonus: €{maria.bonus}")Maria: €3500Bonus: €0The convention is not protection: maria._salary = 0 in the program also goes through, with no error. The underscore tells people "do not touch this from outside", and the child is "family". So the child uses it only when it really has to change the field, not to get around the check in raise_salary().
More precisely, if you are curious
- Two underscores,
self.__salary, are the closest thing toprivate. Python quietly renames the field to_Employee__salary. InManager,self.__salaryis already_Manager__salary, and there is no such field:AttributeError: 'Manager' object has no attribute '_Manager__salary'. This protects against accidental name clashes, not against people: you can still reachmaria._Employee__salary.
Try it
What is missing?
An intern is an employee. What goes in the blank?
class Intern():
passEmployee: the parent goes in the parentheses after the class name.The idea
Parents and a common ancestor
In Java, C# and JavaScript, a class has exactly one parent. Python also allows several: class WorkingStudent(Employee, Student): gets the methods of both. This is called multiple inheritance. Here we will use one parent each. We will need several in the lesson on interfaces.
The chain can be long. Intern inherits from Employee. And what does Employee inherit from, since it has no parent in parentheses? Every class without a parent inherits from object. This is the common parent of all classes. That is why even an empty class has methods:
class Ticket: pass
a = Ticket()b = Ticket()print(a == a)print(a == b)TrueFalse== calls the method __eq__, and it comes from object. For now it only tells you whether the two variables point to the same object.
__str__ comes from there too. If you do not write it, print(ticket) prints something like <__main__.Ticket object at 0x7f3a2c1d5e50>: the class name and an address that is different on every run. In the "Classes and objects" module you wrote your own __str__. Now you know that this replaces the version from object. There is more on this in the next lesson.
More precisely, if you are curious
- In Python there is no class that cannot be inherited from.
typing.finalpromises it, but only a tool like mypy checks it, not Python itself. - With several parents, Python looks for a method in order: first in the class, then in the parents from left to right, and last in
object. You can see the order inWorkingStudent.__mro__.
Exercise
Child classes
Solve the tasks in your editor. The parent classes are from the "Classes and objects" module. If you do not have them at hand, write them again: they are short.
1. Family car. SimpleCar has a speed and a method accelerate() that raises it by 10. Write FamilyCar, which inherits from SimpleCar, starts with 4 seats and has a method add_seat(). Create a family car, speed it up and add a seat:
Speed: 10 km/hSeats: 52. Student. Person has a name, an age and introduce(). Write Student, which inherits from Person and adds an average grade grade (a decimal number). Use the parent constructor.
I am Elena, 19 years old.Grade: 5.53. Breakdance. DanceMove has a name, a duration in seconds and perform(). Write BreakDanceMove with a complexity from 1 to 10 and a method show_complexity().
Performing moonwalk for 5 secondsPerforming windmill for 12 secondswindmill: complexity 8 out of 104. Electric car. Car has a brand, fuel in liters and the kilometers driven. refuel(liters) adds fuel, and drive(distance) uses 1 liter per 10 km and adds the kilometers (if there is not enough fuel, it says so and does not go). Write ElectricCar:
- with no fuel, and with a battery that starts at 100%;
- with an efficiency: how many km it drives on 1% of battery (5 for the "Tesla");
charge()charges the battery to 100%;drive_electric(distance)uses battery instead of fuel and also adds the kilometers.
Car be called? Hint: the card on the underscore.Skoda: 150 km, fuel 35 LTesla: 200 km, battery 60%Not enough battery for 400 kmTesla: 600 km, battery 20%More tasks
- Worker.
Workerinherits fromPersonand adds a job title and a methodwork():
I am Peter, 34 years old.My job: accountant- Intern with a mentor. Add a field with the mentor's name to
Internand pass it in the constructor, together with the name and the salary for the parent. NowInterndoes need its own __init__.
Test