Inheritance
Contents
Inheritance#
Inheritance is an important principle in object-oriented programming. Although we may reach our aims without using inheritance in our own code, it’s important to know the concept and corresponding syntax constructs to understand other people’s code. We’ll meet inheritance related code in
the documentation of modules and packages,
when customizing and extending library code.
Related exercises: Object-Oriented Programming.
Principles of Object-Oriented Programming#
Up to now we only considered three of four fundamental OOP principles:
encapsulation: code is structured into small units (write functions for different tasks and group them together with relevant data into objects)
abstraction:
similar tasks are handled in one and the same way (instead of several unrelated objects we define a class and instantiate several objects sharing the same interface)
implementation details are hidden behind interfaces (a class defines an interface by providing methods and (non-private) member variables; concrete implementation of methods and usage of member variables are not of importance and invisible from outside)
polymorphism (functions and method accept different sets and types of arguments, that is, interfaces are flexible; something a Python programmer does not care about because it’s a very native Python feature, in contrast to C/C++, for instance)
The missing principle is
inheritance (create new classes by extending existing classes)
Idea and Syntax#
Inheritance is a technique to create new classes by extending and/or modifying existing ones. A new class may have a base class. The new class inherits all methods and member variables from its base class and is allowed to replace some of the methods and to add new ones. Syntax:
class NewClass(BaseClass):
def additional_method(self, args):
# do something
def replacement_for_base_class_method(self, args):
# do something
The only difference compared to usual class definitions is in the first line, where a base class can be specified. Defining methods works as before. If the method name does not exist in the base class, then a new method is created. If it already exists in the base class, the new one is used instead of the base class’ method. In addition to explicitly defined methods, the new class inherits all methods from the base class.
Inheritance saves time for implementation and leads to a well structured class hierachy. Object-oriented programming is not solely about defining classes (encapsulation and abstraction), but also about defining meaningful relations between classes, thus, to some extent mapping real world to source code.
Example#
Real-life examples of inheritance often are quite involved. For illustration we use a pathological example resampling relations between geometric objects.
Imagine a vector drawing program. Each geometric object shall be represented as object of a corresponding class. Say quadrangles are objects of type Quad
, paraxial rectangles are objects of type ParRect
and so on. Let’s start with class Point
:
class Point:
''' represent a geometric point in two dimensions '''
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f'({self.x}, {self.y})'
Now we define Quad
:
class Quad:
''' represent a quadrangle '''
def __init__(self, a, b, c, d):
''' make a quad from 4 Point objects '''
self._a = a
self._b = b
self._c = c
self._d = d
def get_points(self):
return (self._a, self._b, self._c, self._d)
def __str__(self):
return f'quadrangle with points' \
f'({self._a.x}, {self._a.y}), ({self._b.x}, {self._b.x}), ' \
f'({self._c.x}, {self._c.x}), ({self._d.x}, {self._d.x})'
The member variables _a
, _b
, _c
, _d
are hidden since we consider them implementation details. If the user wants access to the four points making the quadrangle, get_points
should be called. This way we are free to store the quadrangle in a different format if it seems resonable in future when extending class’ functionality. This is a design decision and is in no way related to inheritance.
Here comes ParRect
: Note that a paraxial rectangle is defined by two Points.
class ParRect(Quad):
def __init__(self, a, c):
''' make a paraxial rect from two points '''
b = Point(c.x, a.y)
d = Point(a.x, c.y)
super().__init__(a, b, c, d)
def __str__(self):
return f'paraxial rect with points ({self._a.x}, {self._a.y}), ({self._c.x}, {self._c.x})'
def area(self):
''' return the rect's area '''
return abs(self._b.x - self._a.x) * abs(self._d.y - self._a.y)
The ParRect
class inherits everything from Quad
. It has a new constructor with fewer arguments than in Quad
, but calls the constructor of Quad
.
Important
The built-in function super
in principle returns self
(that is, the current object), but redirects method calls to the base class.
We reimplement __str__
and add the new method area
. Note that ParRect
objects have member variables _a
, _b
, _c
, _d
since those are created by the Quad
constructor we call in the ParRect
constructor. Also the get_points
method is a member of ParRect
since it gets inherited from Quad
.
parrect = ParRect(Point(0, 0), Point(2, 1))
print(parrect)
print('area: {}'.format(parrect.area()))
a, b, c, d = parrect.get_points()
print('all points:', a, b, c, d)
paraxial rect with points (0, 0), (2, 2)
area: 2
all points: (0, 0) (2, 0) (2, 1) (0, 1)
Type Checking#
Note that isinstance
also returns True
if we check against a base class of an object’s class. In other words, each object is an instance of its class and of all base classes.
print(isinstance(parrect, ParRect))
print(isinstance(parrect, Quad))
True
True
In contrast, type checking with type
returns False
if checked against the base class:
print(type(parrect) == ParRect)
print(type(parrect) == Quad)
True
False
Every Class is a Subclass of object
#
In Python there is a built-in class object
and every newly created class automatically becomes a subclass of object
. The line
class my_new_class:
is equivalent to
class my_new_class(object):
and also to
class my_new_class():
by the way.
To see this in code we might use the built-in function issubclass
. This function returns True
if the first argument is a subclass of the second.
class MyClass:
def __init__(self):
print('Here is __init__()!')
print(issubclass(MyClass, object))
True
Alternatively, we may have a look at the __base__
member variable, which stores the base class:
print(MyClass.__base__)
<class 'object'>
Objects of type object
do not have real functionality. The object
class provides some auxiliary stuff used by the Python interpreter for managing classes and objects.
obj = object()
dir(obj)
['__class__',
'__delattr__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__']
Virtual Methods#
Python does not allow for directly implementing so called virtual methods. A virtual method is a method in a base class which has to be (re-)implemented by each subclass. The typical situation is as follows: The base class implements some functionality, which for some reason has to call a method of a subclass. How to guarantee that the subclass provides the required method?
In Python a virtual method is a usual method which raises a NotImplementedError
, a special exception type like ZeroDivisionError
and so on. If everything is correct, this never happens, because the subclass overrides the base class’ method. But if the creator of the subclass forgets to implement the method required by the base class, an error message will be shown.
Multiple Inheritance#
A class may have several base classes. Just provide a tuple of base classes in the class definition:
class my_class(base1, base2, base3):
The new class inherits everything from all its base classes.
If two base classes provide methods with identical names, the Python interpreter has to decide which one to use for the new class. There is a well-defined algorithm for this decision. If you need this knowledge someday, watch out for method resolution order (MRO).
Exceptions Inherit from Exception
#
Up to now we used built-in exceptions only, like ZeroDivisionError
. But now we have gathered enough knowledge to define new exceptions. Exeptions are classes as we noted before. Each exception is a direct or indirect subclass of BaseException
. Almost all exceptions also are a subclass of Exception
, which itself is a direct subclass of BaseException
. See Exception hierarchy for exceptions’ genealogy.
If we want to introduce a new exception, we have to create a new subclass of Exception
.
class SomeError(Exception):
def __init__(self, message):
self.message = message
def my_function():
print('I do something...')
raise SomeError('Meaty error message!!!')
print('Entering my_function...')
try:
my_function()
except SomeError as error:
print('Exception SomeError: {}'.format(error.message))
Entering my_function...
I do something...
Exception SomeError: Meaty error message!!!
At first we define a new exception class SomeError
. The constructor takes an error message and stores it in the member variable message
.
The function my_function
raises SomeError
.
The main program catches this exception and prints the error message. The as
keyword provides access to a concrete SomeError
object containing the error message.
Note that except SomeBaseClass
also catches all subclasses of SomeBaseClass
. If we want to handle a subclass exception separately we have to place its except
line above the base class’s except
line.
Contrary, a subclass except
never handles a base class exception.