3.5 Inheritance: Introduction and Methods#
In this and the next few sections, we’ll learn about a relationship called inheritance that can exist between two classes. We will focus on one particular way of using inheritance in our code design, and through that, will learn how inheritance works in Python.
Consider a payroll system#
Suppose we are designing an application to keep track of a company’s employees, including some who are paid based on an annual salary and others who are paid based on an hourly wage. We might choose to define a separate class for these two types of employee so that they could have different attributes. For instance, only a salaried employee has a salary to be stored, and only an hourly-paid employee has an hourly wage to be recorded. The classes could also have different methods for the different ways that their pay is computed.
This design overlooks something important: employees of both types have many things in common. For instance, they all have data like a name, address, and employee id. And even though their pay is computed differently, they all get paid. If we had two classes for the two kinds of employees, all the code for these common elements would be duplicated. This is not only redundant but error prone: if you find a bug or make another kind of improvement in one class, you may forget to make the same changes in the other class. Things get even worse if the company decides to add other kinds of employees, such as those working on commission.
Inheritance to the rescue!#
A better design would “factor out” the things that are common to all employees and write them once. This is what inheritance allows us to do, and here is how:
We define a base class that includes the functionality that are common to all employees.
We define a subclass for each specific type of employee. In each subclass, we declare that it is a kind of employee, which will cause it to “inherit” those common elements from the base class without having to define them itself.
Terminology note: if class
B
is a subclass of classA
, we also say thatA
is a superclass ofB
.
In our running example of a company with different kinds of employees, we could define a base class Employee
and two subclasses as follows (for now, we will leave the class bodies empty):
class Employee:
pass
# We use the "(Employee)" part to mark SalariedEmployee as a subclass of Employee.
class SalariedEmployee(Employee):
pass
# We use the "(Employee)" part to mark HourlyEmployee as a subclass of Employee.
class HourlyEmployee(Employee):
pass
Inheritance terminology#
It’s useful to know that there are three ways to talk about classes that are in an inheritance relationship:
base class, superclass, and parent class are synonyms.
derived class, subclass, and child class are synonyms.
Defining methods in a base class#
Now let’s fill in these classes, starting with the methods we want. Once we have that, we can figure out the data that will be needed to implement the methods. To keep our example simple, let’s say that we only need a method for paying an employee, and that it will just print out a statement saying when and how much the employee was paid.
Here’s the outline for the Employee
class with a pay
method.
class Employee:
"""An employee of a company.
"""
def pay(self, pay_date: date) -> None:
"""Pay this Employee on the given date and record the payment.
(Assume this is called once per month.)
"""
pass
If we try to write the body of the pay
method, we run into a problem:
it must compute the appropriate pay amount for printing, and that must be done differently for each type of employee.
Our solution is to pull that part out into a helper method:
class Employee:
"""An employee of a company.
"""
def get_monthly_payment(self) -> float:
"""Return the amount that this Employee should be paid in one month.
Round the amount to the nearest cent.
"""
pass # We still have to figure this out.
def pay(self, pay_date: date) -> None:
"""Pay this Employee on the given date and record the payment.
(Assume this is called once per month.)
"""
payment = self.get_monthly_payment()
print(f'An employee was paid {payment} on {pay_date}.')
Now method pay
is complete, but we have the same problem with get_monthly_payment
:
it has to be different for each type of employee.
Clearly, the subclasses must define this method, each in their own way.
But we are going to leave the incomplete get_monthly_payment
method in the Employee
class,
because it defines part of the interface that every type of Employee
object needs to have.
Subclasses will inherit this incomplete method, which they can redefine as appropriate.
We’ll see how to do that shortly.
Notice that we did as much as we could in the base class, to avoid repeating code in the subclasses.
Making the base class abstract#
Because the Employee
class has a method with no body, client code should not make instances of this incomplete class directly.
We do two things to make this clear:
Change the body of the incomplete method so that it simply raises a
NotImplementedError
. We call such a method an abstract method, and we call a class which has at least one abstract method an abstract class.Add a comment to the class docstring stating that the class is abstract, so that anyone writing client code is warned not to instantiate it.[1]
Here is the complete definition of our class:
class Employee:
"""An employee of a company.
This is an abstract class. Only subclasses should be instantiated.
"""
def get_monthly_payment(self) -> float:
"""Return the amount that this Employee should be paid in one month.
Round the amount to the nearest cent.
"""
raise NotImplementedError
def pay(self, pay_date: date) -> None:
"""Pay this Employee on the given date and record the payment.
(Assume this is called once per month.)
"""
payment = self.get_monthly_payment()
print(f'An employee was paid {payment} on {pay_date}.')
It is possible for client code to ignore the warning and instantiate this class—Python does not prevent it. But look at what happens when we try to call one of the unimplemented methods on the object:
>>> a = Employee()
>>> # This method is itself abstract:
>>> a.get_monthly_payment()
Traceback...
NotImplementedError
>>> # This method calls a helper method that is abstract:
>>> a.pay(date(2018, 9, 30))
Traceback...
NotImplementedError
Subclasses inherit from the base class#
Now let’s fill in class SalariedEmployee
, which is a subclass of Employee
.
Very importantly, all instances of SalariedEmployee
are also instances of Employee
.
We can verify this using the built-in function isinstance
:
>>> # Here we see what isinstance does with an object of a simple built-in type.
>>> isinstance(5, int)
True
>>> isinstance(5, str)
False
>>> # Now let's check how it works with objects of a type that we define.
>>> fred = SalariedEmployee()
>>> # fred's type is as we constructed it: SalariedEmployee.
>>> # More precisely, the object that fred refers to has type SalariedEmployee.
>>> type(fred)
<class 'employee.SalariedEmployee'>
>>> # In other words, the object is an instance of SalariedEmployee.
>>> isinstance(fred, SalariedEmployee)
True
>>> # Here's the important part: it is also an instance of Employee.
>>> isinstance(fred, Employee)
True
Because Python “knows” that fred
is an instance of Employee
, this object will have access to all methods of Employee
!
We say that fred
inherits all of the Employee
methods.
So even if SalariedEmployee
remains an empty class,
its instances can still call the methods get_monthly_payment
and pay
, because they are inherited.
class SalariedEmployee(Employee):
pass
>>> fred = SalariedEmployee()
>>> # fred inherits Employee.get_monthly_payment, and so can call it.
>>> # Of course, it raises an error when called, but it indeed is accessed.
>>> fred.get_monthly_payment()
Traceback...
NotImplementedError
Completing the subclass#
Our SalariedEmployee
and HourlyEmployee
subclasses each inherit two methods:
pay
and get_monthly_payment
.
The method pay
is complete as it is, and is appropriate for all types of employees,
so we needn’t do anything with it.
However, get_monthly_payment
needs a new definition that does not raise an error and
that defines the behaviour appropriately for the particular kind of employee.
We accomplish this simply by defining the method again in the subclass.[2]
We say that this new method definition overrides the inherited definition:
class SalariedEmployee(Employee):
def get_monthly_payment(self) -> float:
# Assuming an annual salary of 60,000
return round(60000.0 / 12.0, 2)
class HourlyEmployee(Employee):
def get_monthly_payment(self) -> float:
# Assuming a 160-hour work month and a $20/hour wage.
return round(160.0 * 20.0, 2)
>>> fred = SalariedEmployee()
>>> fred.get_monthly_payment()
5000.0
>>> jerry = HourlyEmployee()
>>> jerry.get_monthly_payment()
3200.0
We now have a working version of all three classes, albeit a very limited one.
Download and run the code that we've written so far
.
You can experiment with it as you continue reading.
How Python resolves a method name#
The interaction above includes the call fred.get_monthly_payment()
.
Since the name get_monthly_payment
could refer to several possible methods
(one in class Employee
, one in class SalariedEmployee
, and one in class HourlyEmployee
),
we say that the method name must be “resolved”.
To understand inheritance, we need to know how Python handles method resolution in general.
This is how it works: whenever code calls a.myMethod()
, Python determines what type(a)
is and looks in that class for myMethod
.
If myMethod
is found in this class, that method is called; otherwise, Python next searches for myMethod
in the superclass of the type of a
,
and then the superclass of the superclass, etc.,
until it either finds a definition of myMethod
or it has exhausted all possibilities, in which case it raises an AttributeError
.
In the case of the call fred.get_monthly_payment()
,
type(fred)
is SalariedEmployee
, and SalariedEmployee
contains a get_monthly_payment
method.
So that is the one called.
This method call is more interesting: fred.pay(date(2018, 9, 30))
.
The value of type(fred)
is SalariedEmployee
, but class SalariedEmployee
does not contain a pay
method.
So Python next checks in the superclass Employee
, which does contain a pay
method, so then that is called.
Straightforward.
But then inside Employee.pay
, we have the call self.get_monthly_payment()
.
Which get_monthly_payment
is called?
We’re already executing a method (pay
) inside the Employee
class,
but that doesn’t mean we call Employee.get_monthly_payment
.[3]
Remember the rule:
type(self)
determines which class Python first looks in for the method.
At this point, self
is fred
, whose type is SalariedEmployee
, and that class contains a get_monthly_payment
method.
So in this case, when Employee.pay
calls self.get_monthly_payment()
, it gets SalariedEmployee.get_monthly_payment
.