This chapter is taken from the book A Primer on Scientific Programming with Python by H. P. Langtangen, 5th edition, Springer, 2016.
Most of this document tells you how to put related classes together in families such that the family can be viewed as one unit. This idea helps to hide details in a program, and makes it easier to modify or extend the program.
A family of classes is known as a class hierarchy. As in a biological family, there are parent classes and child classes. Child classes can inherit data and methods from parent classes, they can modify these data and methods, and they can add their own data and methods. This means that if we have a class with some functionality, we can extend this class by creating a child class and simply add the functionality we need. The original class is still available and the separate child class is small, since it does not need to repeat the code in the parent class.
The magic of object-oriented programming is that other parts of the
code do not need to distinguish whether an object is the parent or the
child - all generations in a family tree can be treated as a unified
object. In other words, one piece of code can work with all members in
a class family or hierarchy. This principle has revolutionized the
development of large computer systems. As an illustration, two of the
most widely used computer languages today are Java and C#
, and both
of them force programs to be written in an object-oriented style.
The concepts of classes and object-oriented programming first appeared in the Simula programming language in the 1960s. Simula was invented by the Norwegian computer scientists Ole-Johan Dahl and Kristen Nygaard, and the impact of the language is particularly evident in C++, Java, and C#, three of the most dominating programming languages in the world today. The invention of object-oriented programming was a remarkable achievement, and the professors Dahl and Nygaard received two very prestigious prizes: the von Neumann medal and the Turing prize (popularly known as the Nobel prize of computer science).
A parent class is usually called base class or superclass, while the child class is known as a subclass or derived class. We shall use the terms superclass and subclass from now on.
Assume that we have written a class for straight lines, \( y=c_0 + c_1x \):
class Line(object):
def __init__(self, c0, c1):
self.c0 = c0
self.c1 = c1
def __call__(self, x):
return self.c0 + self.c1*x
def table(self, L, R, n):
"""Return a table with n points for L <= x <= R."""
s = ''
import numpy as np
for x in np.linspace(L, R, n):
y = self(x)
s += '%12g %12g\n' % (x, y)
return s
The constructor __init__
initializes the coefficients \( c_0 \) and
\( c_1 \) in the expression for the straight line: \( y=c_0 + c_1x \). The
call operator __call__
evaluates the function \( c_1x+c_0 \), while the
table
method samples the function at n
points and creates a table
of \( x \) and \( y \) values.
A parabola \( y=c_0 + c_1x + c_2x^2 \) contains a straight line as a special case (\( c_2=0 \)). A class for parabolas will therefore be similar to a class for straight lines. All we have do to is to add the new term \( c_2x^2 \) in the function evaluation and store \( c_2 \) in the constructor:
class Parabola(object):
def __init__(self, c0, c1, c2):
self.c0 = c0
self.c1 = c1
self.c2 = c2
def __call__(self, x):
return self.c2*x**2 + self.c1*x + self.c0
def table(self, L, R, n):
"""Return a table with n points for L <= x <= R."""
s = ''
import numpy as np
for x in np.linspace(L, R, n):
y = self(x)
s += '%12g %12g\n' % (x, y)
return s
Observe that we can copy the table
method from class Line
without any modifications.
Python and other languages that support object-oriented programming
have a special construct, so that class Parabola
does not need
to repeat the code that we have already written in class Line
.
We can specify that class Parabola
inherits all code from class
Line
by adding (Line)
in the class headline:
class Parabola(Line):
Class Parabola
now automatically gets all the code from class
Line
. Exercise 1: Demonstrate the magic of inheritance asks you to explicitly demonstrate
the validity of this assertion. We say that class Parabola
is
derived from class Line
, or equivalently, that class Parabola
is
a subclass of its superclass Line
.
Now, class Parabola
should not be identical to class Line
: it
needs to add data in the constructor (for the new term) and to modify
the call operator (because of the new term), but the table
method
can be inherited as it is. If we implement the constructor and the
call operator in class Parabola
, these will override the inherited
versions from class Line
. If we do not implement a table
method,
the one inherited from class Line
is available as if it were coded
visibly in class Parabola
.
Class Parabola
must first have the statements from the class Line
methods __call__
and __init__
, and then add extra code in these
methods. An important principle in computer programming is to avoid
repeating code. We should therefore call up functionality in class
Line
instead of copying statements from class Line
methods to
Parabola
methods. Any method in the superclass Line
can be called
using the syntax
Line.methodname(self, arg1, arg2, ...)
# or
super(Parabola, self).methodname(arg1, arg2, ...)
The latter construction only works if the super class is derived from
Python's general super class object
(i.e., class Line
must be a
new-style class).
Let us now show how to write class Parabola
as a subclass of class
Line
, and implement just the new additional code that we need and
that is not already written in the superclass:
class Parabola(Line):
def __init__(self, c0, c1, c2):
Line.__init__(self, c0, c1) # let Line store c0 and c1
self.c2 = c2
def __call__(self, x):
return Line.__call__(self, x) + self.c2*x**2
This short implementation of class Parabola
provides exactly the
same functionality as the first version of class Parabola
that
we showed in the section A first try on a class for parabolas and that did not inherit
from class Line
. Figure 1 shows the
class hierarchy in UML fashion. The arrow from one class to another
indicates inheritance.
A quick demo of the Parabola
class in a main program,
p = Parabola(1, -2, 2)
p1 = p(x=2.5)
print p1
print p.table(0, 1, 3)
gives this output:
8.5
0 1
0.5 0.5
1 1
The program flow can be somewhat complicated when we work with class hierarchies. Consider the code segment
p = Parabola(1, -1, 2)
p1 = p(x=2.5)
Let us explain the program flow in detail for these two statements.
As always, you can monitor the program flow in a debugger as explained
in the document Debugging in Python [1] or you can
invoke the very illustrative Online Python Tutor.
Calling Parabola(1, -1, 2)
leads to a call to the constructor method
__init__
, where the arguments c0
, c1
, and c2
in this case are
int
objects with values 1, -1, and 2. The self
argument in the
constructor is the object that will be returned and referred to by the
variable p
. Inside the constructor in class Parabola
we call the
constructor in class Line
. In this latter method, we create two
data attributes in the self
object. Printing out dir(self)
will
explicitly demonstrate what self
contains so far in the construction
process. Back in class Parabola
's constructor, we add a third
attribute c2
to the same self
object. Then the self
object is
invisibly returned and referred to by p
.
The other statement, p1 = p(x=2.5)
, has a similar program
flow. First we enter the p.__call__
method with self
as p
and
x
as a float
object with value 2.5. The program flow jumps to the
__call__
method in class Line
for evaluating the linear part
\( c_1x+c_0 \) of the expression for the parabola, and then the flow jumps
back to the __call__
method in class Parabola
where we add the new
quadratic term.
Python has the function isinstance(i,t)
for checking if an
instance i
is of class type t
:
>>> l = Line(-1, 1)
>>> isinstance(l, Line)
True
>>> isinstance(l, Parabola)
False
A Line
is not a Parabola
, but is a Parabola
a
Line
?
>>> p = Parabola(-1, 0, 10)
>>> isinstance(p, Parabola)
True
>>> isinstance(p, Line)
True
Yes, from a class hierarchy perspective, a Parabola
instance is regarded as a
Line
instance too, since it contains everything that a Line
instance contains.
Every instance has an attribute __class__
that holds the
type of class:
>>> p.__class__
<class __main__.Parabola at 0xb68f108c>
>>> p.__class__ == Parabola
True
>>> p.__class__.__name__ # string version of the class name
'Parabola'
Note that p.__class__
is a class
object (or class
definition one may say), while p.__class__.__name__
is a string.
These two variables can be used as an alternative test for
the class type:
if p.__class__.__name__ == 'Parabola':
...
# or
if p.__class__ == Parabola:
...
However, isinstance(p, Parabola)
is the recommended programming style
for checking the type of an object.
A function
issubclass(c1, c2)
tests if class c1
is a subclass of class
c2
, e.g.,
>>> issubclass(Parabola, Line)
True
>>> issubclass(Line, Parabola)
False
The superclasses of a class are stored as a tuple in the
__bases__
attribute of the class object:
>>> p.__class__.__bases__
(<class __main__.Line at 0xb7c5d2fc>,)
>>> p.__class__.__bases__[0].__name__ # extract name as string
'Line'
Instead of letting class Parabola
inherit from a class Line
, we may let it contain a
class Line
instance as a data attribute:
class Parabola(object):
def __init__(self, c0, c1, c2):
self.line = Line(c0, c1) # let Line store c0 and c1
self.c2 = c2
def __call__(self, x):
return self.line(x) + self.c2*x**2
Whether to use inheritance or an attribute depends on the problem
being solved.
If it is natural to say that class Parabola
is a Line
object, we
say that Parabola
has an is-a relationship with class
Line
. Alternatively, if it is natural to think that class Parabola
has a Line
object, we speak about a has-a relationship with
class Line
. In the present example, we may argue that technically
the expression for the parabola is a straight line plus another term
and hence claim an is-a relationship, but we can also view a parabola
as a quantity that has a line plus an extra term, which makes the
has-a relationship relevant.
From a mathematical point of view, many will say that a parabola is
not a line, but that a line is a special case of a parabola.
Adopting this reasoning reverses the dependency of the classes: now it
is more natural to let Line
is a subclass of Parabola
(Line
is
a Parabola
). This easy, and all we have to do is
class Parabola(object):
def __init__(self, c0, c1, c2):
self.c0, self.c1, self.c2 = c0, c2, c2
def __call__(self, x):
return self.c0 + self.c1*x + self.c2*x**2
def table(self, L, R, n): # implemented as shown above
class Line(Parabola):
def __init__(self, c0, c1):
Parabola.__init__(self, c0, c1, 0)
The inherited __call__
method from class Parabola
will
work since the c2
coefficient is zero.
Exercise 4: Create an alternative class hierarchy for polynomials suggests deriving Parabola
from
a general class Polynomial
and asks you to discuss the alternative class
designs.
In the example where Parabola
as a subclass of Line
, we
used inheritance to extend the functionality of the superclass.
The case where Line
is a subclass of Parabola
is an example on
restricting the superclass functionality in a subclass.
How classes depend on each other is influenced by two factors: sharing
of code and logical relations. From a sharing of code perspective,
many will say that class Parabola
is naturally a subclass of Line
,
the former adds code to the latter. On the other hand, Line
is
naturally a subclass of Parabola
from the logical relations in
mathematics. Computational efficiency is a third perspective when we
implement mathematics. When Line
is a subclass of Parabola
we
always evaluate the \( c_2x^2 \) term in the parabola although this term
is zero. Nevertheless, when Parabola
is a subclass of Line
, we
call Line.__call__
to evaluate the linear part of the second-degree
polynomial, and this call is costly in Python. From a pure efficiency
point of view, we would reprogram the linear part in
Parabola.__call__
(which is against the programming habit we have
been arguing for!). This little discussion here highlights the many
different considerations that come into play when establishing class
relations.
As another example of class hierarchies, we now want to represent
functions by classes, as described in the document
Introduction to classes in Python
[2], but in addition to the __call__
method, we also want to provide methods for the first and second
derivative. The class can be sketched as
class SomeFunc(object):
def __init__(self, parameter1, parameter2, ...)
# Store parameters
def __call__(self, x):
# Evaluate function
def df(self, x):
# Evaluate the first derivative
def ddf(self, x):
# Evaluate the second derivative
For a given function, the analytical expressions for first and second
derivative must be manually coded. However, we could think of
inheriting general functions for computing these derivatives
numerically, such that the only thing we must always implement is the
function itself. To realize this idea, we create a superclass
class FuncWithDerivatives(object):
def __init__(self, h=1.0E-5):
self.h = h # spacing for numerical derivatives
def __call__(self, x):
raise NotImplementedError\
('___call__ missing in class %s' % self.__class__.__name__)
def df(self, x):
"""Return the 1st derivative of self.f."""
# Compute first derivative by a finite difference
h = self.h
return (self(x+h) - self(x-h))/(2.0*h)
def ddf(self, x):
"""Return the 2nd derivative of self.f."""
# Compute second derivative by a finite difference:
h = self.h
return (self(x+h) - 2*self(x) + self(x-h))/(float(h)**2)
This class is only meant as a superclass of other classes.
For a particular function, say \( f(x)=\cos (ax)+x^3 \), we represent it by
a subclass:
class MyFunc(FuncWithDerivatives):
def __init__(self, a):
self.a = a
def __call__(self, x):
return cos(self.a*x) + x**3
def df(self, x):
a = self.a
return -a*sin(a*x) + 3*x**2
def ddf(self, x):
a = self.a
return -a*a*cos(a*x) + 6*x
The superclass constructor is never called, hence h
is never
initialized, and there are no possibilities for using numerical
approximations via the superclass methods
df
and ddf
.
Instead, we override all the inherited methods and implement our own
versions.
Many think it is a good programming style to always call the superclass constructor in a subclass constructor, even in simple classes where we do not need the functionality of the superclass constructor.
For a more complicated function, e.g., \( f(x)=\ln |p\tanh(qx\cos rx)| \), we may skip the analytical derivation of the derivatives, and just code \( f(x) \) and rely on the difference approximations inherited from the superclass to compute the derivatives:
class MyComplicatedFunc(FuncWithDerivatives):
def __init__(self, p, q, r, h=1.0E-5):
FuncWithDerivatives.__init__(self, h)
self.p, self.q, self.r = p, q, r
def __call__(self, x):
return log(abs(self.p*tanh(self.q*x*cos(self.r*x))))
That's it! We are now ready to use this class:
>>> f = MyComplicatedFunc(1, 1, 1)
>>> x = pi/2
>>> f(x)
-36.880306514638988
>>> f.df(x)
-60.593693618216086
>>> f.ddf(x)
3.3217246931444789e+19
Class MyComplicatedFunc
inherits the df
and ddf
methods from the
superclass FuncWithDerivatives
. These methods compute the first and
second derivatives approximately, provided that we have defined a
__call__
method. If we fail to define this method, we will inherit
__call__
from the superclass, which just raises an exception, saying
that the method is not properly implemented in class
MyComplicatedFunc
.
The important message in this subsection is that we introduced a super
class to mainly define an interface, i.e., the operations (in terms
of methods) that one can do with a class in this class hierarchy. The
superclass itself is of no direct use, since it does not implement any
function evaluation in the __call__
method. However, it stores a
variable common to all subclasses (h
), and it implements general
methods df
and ddf
that any subclass can make use of. A specific
mathematical function must be represented as a subclass, where the
programmer can decide whether analytical derivatives are to be used,
or if the more lazy approach of inheriting general functionality (df
and ddf
) for computing numerical derivatives is satisfactory.
In object-oriented programming, the superclass very often defines an interface, and instances of the superclass have no applications on their own - only instances of subclasses can do anything useful.
To digest the present material on inheritance, we recommend doing Exercise 1: Demonstrate the magic of inheritance-Exercise 4: Create an alternative class hierarchy for polynomials before reading the next section.