3.8 Inheritance: Thoughts on Design#
Now that you understand the mechanics of inheritance, let’s go back and make some final comments.
Four things we can do with an inherited method#
When a subclass inherits a method from its superclass, there are four things we can choose to do in the subclass.
1. Simply inherit an implemented method#
If a method has been implemented in the superclass and its behaviour is appropriate for the subclass,
then we can simply use this behaviour by choosing not to override the method.
For example, HourlyEmployee
does not define a pay
method, so it simply inherits the pay
method from Employee
.
Any time we call pay
on an instance of HourlyEmployee
, the Employee.pay
method is called.
Of course, we should never do this when the method is abstract, because a subclass must override every abstract method to implement it properly.
2. Override an abstract method to implement it#
When a method has not been implemented in the superclass (its body is just raise NotImplementedError
), the method must be overridden in the subclass in order to provide an implementation.[1]
For example, SalariedEmployee
and HourlyEmployee
must both implement the abstract get_monthly_payment
method.
3. Override an implemented method to replace it#
If a method has been implemented in the superclass,
but the subclass requires a different behaviour,
the subclass can override the method and provide a completely different implementation.
This is something we haven’t yet seen, but is very simple.
For example, we could override the pay
method in SalariedEmployee
:
class SalariedEmployee(Employee):
def get_monthly_payment(self) -> float:
# Assuming an annual salary of 60,000
return round(60000 / 12, 2)
def pay(self, pay_date: date) -> None:
print('Payment rejected! Mwahahahaha.')
>>> fred = SalariedEmployee()
>>> fred.pay(date(2017, 9, 30))
Payment rejected! Mwahahahaha.
4. Override an implemented method to extend it#
Sometimes we want the behaviour that was defined in the superclass, but we want to add some other behaviour.
In other words, we want to extend the inherited behaviour.
We have witnessed this in the initializers for our payroll system.
The Employee
initializer takes care of instance attributes that are common to all employees.
Rather than repeat that code, each subclass initializer calls it as a helper and then has additional code to initialize additional instance attributes that are specific to that subclass.
We can extend any inherited method, not just an initializer.
Here’s an example.
Suppose at pay time we wanted to print out two messages, the original one from Employee
, and also a SalariedEmployee
-specific message.
Since we already have a superclass method that does part of the work,
we can call it as a helper method instead of repeating its code:
class SalariedEmployee(Employee):
def pay(self, pay_date: date) -> None:
Employee.pay(self, pay_date) # Call the superclass method as a helper.
print('Payment accepted! Have a nice day. :)')
>>> fred = SalariedEmployee()
>>> fred.pay(date(2017, 9, 30))
An employee was paid 3200 on September 30, 2017.
Payment accepted! Have a nice day. :)
Abstract classes are useful#
The Employee
class is abstract, and client code should never instantiate it.
Is it therefore useless?
No, quite the opposite!
We’ve already seen that it defines a shared public interface that client code can count on,
and as a result, supports polymorphism.
Furthermore, polymorphic client code will continue to work even if new subclasses are written in the future!
Our abstract Employee
class is useful in a second way.
If and when someone does decide to write another subclass of Employee
,
for instance for employees who are paid a commission,
the programmer knows that the abstract method get_monthly_payment
must be implemented.
In other words, they must support the shared public interface that the client code counts on.
We can think of this as providing helpful guidance for the programmer writing the new subclass.
When to use inheritance#
We’ve seen some benefits of inheritance.
However, inheritance isn’t perfect for every situation.
Don’t forget the other kind of relationship between classes that we’ve seen: composition.
For example, to represent people who are car owners, a Person
object might have an attribute car
which stores a reference to a Car
object.
We wouldn’t use inheritance to represent the relationship between Person
and Car
!
Composition is commonly thought of as a “has a” relationship. For example, a person “has a” car. Inheritance is thought of as an “is a” relationship. For example, a salaried employee “is an” employee. Of course, the “has a” vs. “is a” categorization is rather simplistic, and not every real-world problem is so clearly defined.
When we use inheritance, any change in a superclass affects all of its subclasses, which can lead to unintended effects. To avoid this complexity, in this course we’ll stick to using inheritance in the traditional “shared public interface” sense. Moreover, we will often prefer that a subclass not change the public interface of a superclass at all:
not by changing the interface of any public methods (e.g., adding/removing parameters, or changing their types)
not by adding new public methods or attributes to a subclass (of course, adding private attributes or methods is acceptable)
As a general programming concept, inheritance has many other uses, and you’ll learn about some of them in CSC207, Software Design.