This chapter is taken from the book A Primer on Scientific Programming with Python by H. P. Langtangen, 5th edition, Springer, 2016.
A subclass inherits everything from its superclass, in particular all data attributes and methods. The subclass can add new data attributes, overload methods, and thereby enrich or restrict functionality of the superclass.
Consider class Gravity
from the final section of the document
Introduction to classes in Python
[2] for representing the gravity force
\( GMm/r^2 \) between two masses \( m \) and \( M \) being a distance \( r \) apart.
Suppose we want to make a class for the electric force between two
charges \( q_1 \) and \( q_2 \), being a distance \( r \) apart in a medium with
permittivity \( \epsilon_0 \) is \( Gq_1q_2/r^2 \), where
\( G^{-1}=4\pi\epsilon_0 \). We use the approximate value \( G=8.99\cdot
10^9\hbox{ Nm}^2/\hbox{C}^2 \) (C is the Coulomb unit used to measure
electric charges such as \( q_1 \) and \( q_2 \)). Since the electric force
is similar to the gravity force, we can easily implement the electric
force as a subclass of Gravity
. The implementation just needs to
redefine the value of \( G \)!
class CoulombsLaw(Gravity):
def __init__(self, q1, q2):
Gravity.__init__(self, q1, q2)
self.G = 8.99E9
We can now call the inherited force(r)
method to compute the electric
force and the visualize
method to make a plot of the force:
c = CoulombsLaw(1E-6, -2E-6)
print 'Electric force:', c.force(0.1)
c.visualize(0.01, 0.2)
However, the plot
method inherited from class Gravity
has
an inappropriate title referring to "Gravity force" and the masses
\( m \) and \( M \). An easy fix could be to have the plot title as a data
attribute set in the constructor. The subclass can then override
the contents of this attribute, as it overrides self.G
.
It is quite common to discover that a class needs adjustments if it
is to be used as superclass.
The typical sketch of creating a subclass goes as follows:
class SuperClass(object):
def __init__(self, p, q):
self.p, self.q = p, q
def where(self):
print 'In superclass', self.__class__.__name__
def compute(self, x):
self.where()
return self.p*x + self.q
class SubClass(SuperClass):
def __init__(self, p, q, a):
SuperClass.__init__(self, p, q)
self.a = a
def where(self):
print 'In subclass', self.__class__.__name__
def compute(self, x):
self.where()
return SuperClass.compute(self, x) + self.a*x**2
This example shows how a subclass extends a superclass with one
data attribute (a
). The subclass' compute
method calls the
corresponding superclass method, as well as the overloaded method
where
. Let us invoke the compute
method through superclass and
subclass instances:
>>> super = SuperClass(1, 2)
>>> sub = SubClass(1, 2, 3)
>>> v1 = super.compute(0)
In superclass SuperClass
>>> v2 = sub.compute(0)
In subclass SubClass
In subclass SubClass
Observe that in the subclass sub
, method compute
calls
self.where
, which translates to the where
method in
SubClass
. Then the compute
method in SuperClass
is invoked, and this method also makes a self.where
call,
which is a call to SubClass
' where
method (think of
what self
is here, it is sub
, so it is natural that
we get where
in the subclass (sub.where
) and not
where
in the superclass part of sub
).
In this example, classes SuperClass
and SubClass
constitute a
class hierarchy. Class SubClass
inherits the attributes p
and q
from its superclass, and overrides the methods where
and compute
.
The important computer science topics in this document are
The summarizing example of this document concerns a class hierarchy
for simplifying reading input data into programs. Input data may come
from several different sources: the command line, a file, or from a
dialog with the user, either of input
form or in a graphical user
interface (GUI). Therefore it makes sense to create a class hierarchy
where subclasses are specialized to read from different sources and
where the common code is placed in a superclass. The resulting tool
will make it easy for you to let your programs read from many
different input sources by adding just a few lines.
Let us motivate the problem by a case where we want to write a program for dumping \( n \) function values of \( f(x) \) to a file for \( x\in [a,b] \). The core part of the program typically reads
import numpy as np
with open(filename, 'w') as outfile:
for x in np.linspace(a, b, n):
outfile.write('%12g %12g\n' % (x, f(x)))
Our purpose is to read data into the variables a
, b
, n
,
filename
, and f
. For the latter we want to specify a
formula and use the StringFunction
tool
(see pydoc scitools.StringFunction.StringFunction
)
to make the function f
:
from scitools.StringFunction import StringFunction
f = StringFunction(formula)
How can we read a
, b
, n
, formula
, and filename
conveniently into the program?
The basic idea is that we place the input data in a dictionary, and create a tool that can update this dictionary from sources like the command line, a file, a GUI, etc. Our dictionary is then
p = dict(formula='x+1', a=0, b=1, n=2, filename='tmp.dat')
This dictionary specifies the names of the input parameters
to the program and the default values of these parameters.
Using the tool is a matter of feeding p
into the constructor
of a subclass in the tools' class hierarchy and extract the parameters
into, for example, distinct variables:
inp = Subclassname(p)
a, b, filename, formula, n = inp.get_all()
Depending on what we write as Subclassname
, the five
variables can be read from the command line, the terminal window,
a file, or a GUI.
The task now is to implement a class hierarchy to facilitate
the described flexible reading of input data.
We first create a very simple superclass ReadInput
. Its main purpose
is to store the parameter dictionary as a data attribute, provide a
method get
to extract single values, and a method get_all
to extract all parameters into distinct variables:
class ReadInput(object):
def __init__(self, parameters):
self.p = parameters
def get(self, parameter_name):
return self.p[parameter_name]
def get_all(self):
return [self.p[name] for name in sorted(self.p)]
def __str__(self):
import pprint
return pprint.pformat(self.p)
Note that we in the get_all
method must sort the keys in
self.p
such that the list of returned variables is well defined.
In the calling program we can then list variables in the same
order as the alphabetic order of the parameter names, for example:
a, b, filename, formula, n = inp.get_all()
The __str__
method applies the pprint
module to
get a pretty print of all the parameter names and their values.
Class ReadInput
cannot read from any source - subclasses are supposed
to do this. The forthcoming text describes various types of subclasses
for various types of reading input.
The perhaps simplest way of getting data into a program is
to use raw_input
. We then prompt the user with a text Give name:
and get an appropriate object back (recall that strings must be
enclosed in quotes). The subclass PromptUser
for doing this
then reads
class PromptUser(ReadInput):
def __init__(self, parameters):
ReadInput.__init__(self, parameters)
self._prompt_user()
def _prompt_user(self):
for name in self.p:
self.p[name] = eval(raw_input("Give " + name + ": "))
Note the underscore in _prompt_user
: the underscore signifies
that this is a "private" method in the PromptUser
class, not
intended to be called by users of the class.
There is a major difficulty with using eval
on the input from the
user. When the input is intended to be a string object, such as a
filename, say tmp.inp
, the program will perform the operation
eval(tmp.inp)
, which leads to an exception because tmp.inp
is
treated as a variable inp
in a module tmp
and not as the string
'tmp.inp'
. To solve this problem, we use the str2obj
function
from the scitools.misc
module. This function will return the right
Python object also in the case where the argument should result in a
string
object.
The bottom line is that str2obj
acts as a safer
eval(raw_input(...))
call. The key assignment in class PromptUser
is then changed to
self.p[name] = str2obj(raw_input("Give " + name + ": "))
We can also place name = value
commands in a file and load this
information into the dictionary self.p
. An example of a file can be
formula = sin(x) + cos(x)
filename = tmp.dat
a = 0
b = 1
In this example we have omitted n
, so we rely on its default value.
A problem is how to give the filename. The easy way out of this
problem is to read from standard input, and just redirect standard
input from a file when we run the program. For example, if the
filename is tmp.inp
, we run the program as follows in a terminal
window
Terminal> python myprog.py < tmp.inp
(The redirection
of standard input from a file does not work in IPython so we are in this
case forced
to run the program in a terminal window.)
To interpret the contents of the file, we read line by line, split
each line with respect to =
, use the left-hand side as the parameter
name and the right-hand side as the corresponding value. It is
important to strip away unnecessary blanks in the name and value. The
complete class now reads
class ReadInputFile(ReadInput):
def __init__(self, parameters):
ReadInput.__init__(self, parameters)
self._read_file()
def _read_file(self, infile=sys.stdin):
for line in infile:
if "=" in line:
name, value = line.split("=")
self.p[name.strip()] = str2obj(value.strip())
A nice feature with reading from standard input is that if we do not
redirect standard input to a file, the program will prompt the user in
the terminal window, where the user can give commands of the type
name = value
for setting selected input data. A Ctrl+d
is needed
to terminate the interactive session in the terminal window and
continue execution of the program.
For input from the command line we assume that parameters and values are given as option-value pairs, e.g., as in
--a 1 --b 10 --n 101 --formula "sin(x) + cos(x)"
We apply the argparse
module
to parse the command-line arguments. The list of legal option names
must be constructed from the list of keys in the self.p
dictionary.
The complete class takes the form
class ReadCommandLine(ReadInput):
def __init__(self, parameters):
self.sys_argv = sys.argv[1:] # copy
ReadInput.__init__(self, parameters)
self._read_command_line()
def _read_command_line(self):
parser = argparse.ArgumentParser()
# Make argparse list of options
for name in self.p:
# Default type: str
parser.add_argument('--'+name, default=self.p[name])
args = parser.parse_args()
for name in self.p:
self.p[name] = str2obj(getattr(args, name))
import Tkinter
try:
We could specify the type of a parameter as type(self.p[name])
or
self.p[name].__class__
, but if a float
parameter has been given an
integer default value, the type will be int
and argparse
will not
accept a decimal number as input. Our more general strategy is to drop
specifying the type, which implies that all parameters in the args
object become strings. We then use the str2obj
function to convert
to the right type, a technique that is used throughout the ReadInput
module.
We can with a little extra effort also make a graphical user interface
(GUI) for reading the input data. An example of a user interface is
displayed in Figure 13. Since the
technicalities of the implementation is beyond the scope of this
document, we do not show the subclass GUI
that creates the GUI and
loads the user input into the self.p
dictionary.
Some extra flexibility can easily be added to the get
method
in the superclass. Say we want to extract a variable number of
parameters:
a, b, n = inp.get('a', 'b', 'n') # 3 variables
n = inp.get('n') # 1 variable
The key to this extension is to use a variable number of arguments as
explained in the document Variable number of
function arguments in Python
[5]:
class ReadInput(object):
...
def get(self, *parameter_names):
if len(parameter_names) == 1:
return self.p[parameter_names[0]]
else:
return [self.p[name] for name in parameter_names]
Let us show how we can use the classes in the ReadInput
hierarchy. We apply the motivating example described earlier. The
name of the program is demo_ReadInput.py. As first command-line argument it
takes the name of the input source, given as the name of a subclass in
the ReadInput
hierarchy. The code for loading input data from any of
the sources supported by the ReadInput
hierarchy goes as follows:
p = dict(formula='x+1', a=0, b=1, n=2, filename='tmp.dat')
from ReadInput import *
input_reader = eval(sys.argv[1]) # PromptUser, ReadInputFile, ...
del sys.argv[1] # otherwise argparse don't like our extra option
inp = input_reader(p)
a, b, filename, formula, n = inp.get_all()
print inp
Note how convenient eval
is to automatically create the right
subclass for reading input data.
Our first try on running this program applies the PromptUser
class:
demo_ReadInput.py PromptUser
Give a: 0
Give formula: sin(x) + cos(x)
Give b: 10
Give filename: function_data
Give n: 101
{'a': 0,
'b': 10,
'filename': 'function_data',
'formula': 'sin(x) + cos(x)',
'n': 101}
The next example reads data from a file tmp.inp
with the same
contents as shown in paragraph above about reading from file.
Terminal> demo_ReadInput.py ReadFileInput < tmp.inp
{'a': 0, 'b': 1, 'filename': 'tmp.dat',
'formula': 'sin(x) + cos(x)', 'n': 2}
We can also drop the redirection of standard input to a file, and
instead run an interactive session in IPython or the terminal window:
demo_ReadInput.py ReadFileInput
n = 101
filename = myfunction_data_file.dat
^D
{'a': 0,
'b': 1,
'filename': 'myfunction_data_file.dat',
'formula': 'x+1',
'n': 101}
Note that Ctrl+d
is needed to end the interactive
session with the user and continue program execution.
Command-line arguments can also be specified:
demo_ReadInput.py ReadCommandLine \
--a -1 --b 1 --formula "sin(x) + cos(x)"
{'a': -1, 'b': 1, 'filename': 'tmp.dat',
'formula': 'sin(x) + cos(x)', 'n': 2}
Finally, we can run the program with a GUI,
demo_ReadInput.py GUI
{'a': -1, 'b': 10, 'filename': 'tmp.dat',
'formula': 'x+1', 'n': 2}
The GUI is shown in Figure 13.
Fortunately, it is now quite obvious how to apply the ReadInput
hierarchy of classes in your own programs to simplify input.
Especially in applications with a large number of parameters one can
initially define these in a dictionary and then automatically create
quite comprehensive user interfaces where the user can specify only
some subset of the parameters (if the default values for the rest of
the parameters are suitable).