$$ \newcommand{\tp}{\thinspace .} $$

 

 

 

This chapter is taken from the book A Primer on Scientific Programming with Python by H. P. Langtangen, 5th edition, Springer, 2016.

Summary

Chapter topics

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.

Subclass example

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.

Subclassing in general

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.

Terminology

The important computer science topics in this document are

Example: Input data reader

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.

Problem

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.

Solution

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.

Prompting the user

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 + ": "))

Reading from file

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.

Reading from the command line

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.

Reading from a gui

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.


Figure 13: Screen dump of a graphical user interface to read input data into a program (class GUI in the ReadInput hierarchy).

More flexibility in the superclass

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]

Demonstrating the tool

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).