This chapter is taken from the book A Primer on Scientific Programming with Python by H. P. Langtangen, 5th edition, Springer, 2016.
Classes can be used for many things in scientific computations, but one of the most frequent programming tasks is to represent mathematical functions that have a set of parameters in addition to one or more independent variables. The section Challenge: functions with parameters explains why such mathematical functions pose difficulties for programmers, and the section Representing a function as a class shows how the class idea meets these difficulties. The sections Another function class example presents another example where a class represents a mathematical function. More advanced material about classes, which for some readers may clarify the ideas, but which can also be skipped in a first reading, appears in the sections Alternative function class implementations and the section Making classes without the class construct.
To motivate for the class concept, we will look at functions with parameters. One example is \( y(t)=v_0t-\frac{1}{2}gt^2 \). Conceptually, in physics, the \( y \) quantity is viewed as a function of \( t \), but \( y \) also depends on two other parameters, \( v_0 \) and \( g \), although it is not natural to view \( y \) as a function of these parameters. We may write \( y(t;v_0,g) \) to indicate that \( t \) is the independent variable, while \( v_0 \) and \( g \) are parameters. Strictly speaking, \( g \) is a fixed parameter (as long as we are on the surface of the earth and can view \( g \) as constant), so only \( v_0 \) and \( t \) can be arbitrarily chosen in the formula. It would then be better to write \( y(t;v_0) \).
In the general case, we may have a function of \( x \) that has \( n \) parameters \( p_1,\ldots,p_n \): \( f(x; p_1,\ldots,p_n) \). One example could be $$ \begin{equation*} g(x; A, a) = Ae^{-ax} \thinspace . \end{equation*} $$
How should we implement such functions? One obvious way is to have the independent variable and the parameters as arguments:
def y(t, v0):
g = 9.81
return v0*t - 0.5*g*t**2
def g(x, a, A):
return A*exp(-a*x)
There is one major problem with this solution. Many software tools we can use for mathematical operations on functions assume that a function of one variable has only one argument in the computer representation of the function. For example, we may have a tool for differentiating a function \( f(x) \) at a point \( x \), using the approximation $$ \begin{equation} f'(x)\approx {f(x+h)-f(x)\over h} \tag{1} \end{equation} $$ coded as
def diff(f, x, h=1E-5):
return (f(x+h) - f(x))/h
The diff
function works with any function f
that takes
one argument:
def h(t):
return t**4 + 4*t
dh = diff(h, 0.1)
from math import sin, pi
x = 2*pi
dsin = diff(sin, x, h=1E-6)
Unfortunately, diff
will not work with our y(t, v0)
function. Calling diff(y, t)
leads to an error inside the
diff
function, because it tries to call our y
function with
only one argument while the y
function requires two.
Writing an alternative diff
function for
f
functions having two arguments is a bad remedy as it
restricts the set of admissible f
functions to the very special
case of a function with one independent variable and one parameter.
A fundamental principle in computer programming is to strive for
software that is as general and widely applicable as possible.
In the present case, it means that the diff
function should be
applicable to all functions f
of one variable, and letting
f
take one argument is then the natural decision to make.
The mismatch of function arguments, as outlined above, is a major problem because a lot of software libraries are available for operations on mathematical functions of one variable: integration, differentiation, solving \( f(x)=0 \), finding extrema, etc. All these libraries will try to call the mathematical function we provide with only one argument. When our function has more arguments, the code inside the library aborts in the call to our function, and such errors may not always be easy to track down.
The requirement is thus to define Python implementations of mathematical functions of one variable with one argument, the independent variable. The two examples above must then be implemented as
def y(t):
g = 9.81
return v0*t - 0.5*g*t**2
def g(t):
return A*exp(-a*x)
These functions work only if
v0
, A
, and a
are global variables,
initialized before one attempts to call the functions.
Here are two sample calls where diff
differentiates y
and g
:
v0 = 3
dy = diff(y, 1)
A = 1; a = 0.1
dg = diff(g, 1.5)
The use of global variables is in general considered bad programming.
Why global variables are problematic in the present case
can be illustrated when there is need to work with several
versions of a function. Suppose we want to work with two versions
of \( y(t;v_0) \), one with \( v_0=1 \) and one with \( v_0=5 \).
Every time we call y
we must remember which version of the function
we work with, and set v0
accordingly prior to the call:
v0 = 1; r1 = y(t)
v0 = 5; r2 = y(t)
Another problem is that
variables with simple names like v0
, a
, and A
may
easily be used as global variables in other parts of the program.
These parts may change our v0
in a context different from the
y
function, but the change affects the correctness of the
y
function. In such a case, we say that changing v0
has
side effects, i.e., the change affects other parts of the program
in an unintentional way.
This is one reason why a golden rule of programming tells us to limit the
use of global variables as much as possible.
Another solution to the problem of needing two \( v_0 \) parameters
could be to introduce two y
functions, each with
a distinct \( v_0 \) parameter:
def y1(t):
g = 9.81
return v0_1*t - 0.5*g*t**2
def y2(t):
g = 9.81
return v0_2*t - 0.5*g*t**2
Now we need to initialize v0_1
and v0_2
once, and then
we can work with y1
and y2
.
However, if we need 100 \( v_0 \) parameters, we need 100 functions.
This is tedious to code, error prone, difficult to administer, and
simply a really bad solution to a programming problem.
So, is there a good remedy? The answer is yes: the class concept solves all the problems described above!
A class contains a set of variables (data) and a set of functions, held together as one unit. The variables are visible in all the functions in the class. That is, we can view the variables as "global" in these functions. These characteristics also apply to modules, and modules can be used to obtain many of the same advantages as classes offer (see comments in the section Making classes without the class construct). However, classes are technically very different from modules. You can also make many copies of a class, while there can be only one copy of a module. When you master both modules and classes, you will clearly see the similarities and differences. Now we continue with a specific example of a class.
Consider the function \( y(t; v_0)=v_0t - \frac{1}{2}gt^2 \).
We may say that \( v_0 \) and \( g \), represented by the variables
v0
and g
, constitute the data. A Python function,
say value(t)
, is needed to compute the value of
\( y(t;v_0) \) and this function
must have access
to the data v0
and g
, while t
is an argument.
A programmer experienced with classes will then suggest to collect
the data v0
and g
, and the function value(t)
,
together as a class. In addition, a class usually has another function,
called constructor for initializing
the data. The constructor
is always named __init__
.
Every class must have a name, often starting with a capital, so we
choose Y
as the name since the class represents a mathematical
function with name \( y \).
Figure 1 sketches the contents of class Y
as a so-called UML diagram, here created
with aid of the
program class_Y_v1_UML.py.
The UML diagram has two "boxes", one where the functions are listed,
and one where the variables are listed.
Our next step is to implement this class in Python.
The complete code for our
class Y
looks as follows in Python:
class Y:
def __init__(self, v0):
self.v0 = v0
self.g = 9.81
def value(self, t):
return self.v0*t - 0.5*self.g*t**2
A puzzlement for newcomers to Python classes is the self
parameter, which may take some efforts and time to fully understand.
Before we dig into what each line in the class implementation means, we start by showing how the class can be used to compute values of the mathematical function \( y(t;v_0) \).
A class creates a new data type, here of name Y
,
so when we use the class to make
objects, those objects are of type Y
. (Actually,
all the standard Python objects, such as
lists, tuples, strings, floating-point numbers, integers,
etc., are built-in
Python classes, with names list
, tuple
,
str
, float
,
int
, etc.)
An object of a user-defined class (like Y
) is usually called
an instance.
We need such an instance in order to use the data in the class and call the
value
function.
The following statement constructs an instance bound to the variable
name y
:
y = Y(3)
Seemingly, we call the class Y
as if it were a function.
Actually, Y(3)
is automatically translated by Python to
a call to the constructor __init__
in class Y
.
The arguments in the call, here only the number 3
,
are always passed on as
arguments to __init__
after the self
argument. That is, v0
gets the value 3
and self
is just dropped in the call. This may be confusing, but it is a rule
that the self
argument is never used in calls to
functions in classes.
With the instance y
, we can compute the value \( y(t=0.1;v_0=3) \) by the
statement
v = y.value(0.1)
Here also, the self
argument is dropped in the call to value
.
To access functions and variables in a class, we must prefix the
function and variable names by the name of the instance and a dot:
the value
function is reached as y.value
, and the
variables are reached as y.v0
and y.g
. We can, for example,
print the value of v0
in the instance y
by writing
print y.v0
The output will in this case be 3
.
We have already introduced the term "instance'' for the object of a class.
Functions in classes are commonly called methods,
and variables (data) in classes are called
data attributes. Methods are also known as method attributes.
From now on we will use this terminology. In our sample class Y
we have two methods or method attributes, __init__
and value
, two
data attributes, v0
and g
, and
four attributes
in total (__init__
, value
, v0
, and g
).
The names of attributes can be chosen freely, just as
names of ordinary Python functions and variables. However, the constructor
must have the name __init__
, otherwise it is not automatically
called when we create new instances.
You can do whatever you want in whatever method, but it is a common convention to use the constructor for initializing the variables in the class.
We can have as many attributes as we like in a class, so
let us add a new method to class Y
. This method is called
formula
and prints a string containing the formula of
the mathematical function \( y \). After this formula, we provide the
value of \( v_0 \). The string can then be
constructed as
'v0*t - 0.5*g*t**2; v0=%g' % self.v0
where self
is an instance of class Y
.
A call of formula
does not need any arguments:
print y.formula()
should be enough to create, return, and print the string.
However, even if the formula
method does not need any arguments, it
must have a self
argument, which is left out in the call
but needed inside the method to access the attributes.
The implementation of the method is therefore
def formula(self):
return 'v0*t - 0.5*g*t**2; v0=%g' % self.v0
For completeness, the whole class now reads
class Y:
def __init__(self, v0):
self.v0 = v0
self.g = 9.81
def value(self, t):
return self.v0*t - 0.5*self.g*t**2
def formula(self):
return 'v0*t - 0.5*g*t**2; v0=%g' % self.v0
Example on use may be
y = Y(5)
t = 0.2
v = y.value(t)
print 'y(t=%g; v0=%g) = %g' % (t, y.v0, v)
print y.formula()
with the output
y(t=0.2; v0=5) = 0.8038
v0*t - 0.5*g*t**2; v0=5
A common mistake done by newcomers to the class construction is to place
the code that applies the class at the same indentation as the class methods.
This is illegal. Only method definitions and assignments to
so-called static data attributes
(the section Static methods and attributes) can appear in the indented block under
the class
headline.
Ordinary data attribute assignment must be done inside methods.
The main program using the class must appear with the same indent as
the class
headline.
We may create several \( y \) functions with different values of \( v_0 \):
y1 = Y(1)
y2 = Y(1.5)
y3 = Y(-3)
We can treat y1.value
, y2.value
, and
y3.value
as ordinary Python functions of t
, and then pass
them on to any Python function that expects a function of one variable.
In particular, we can send the functions to the diff(f, x)
function
from the section Challenge: functions with parameters:
dy1dt = diff(y1.value, 0.1)
dy2dt = diff(y2.value, 0.1)
dy3dt = diff(y3.value, 0.2)
Inside the diff(f, x)
function, the argument
f
now behaves as a function
of one variable that automatically
carries with it two variables v0
and g
.
When f
refers to (e.g.) y3.value
, Python actually
knows that f(x)
means y3.value(x)
, and inside the y3.value
method
self
is y3
, and we have
access to y3.v0
and y3.g
.
When use Python version 2 and write a class like
class V:
...
we get what is known as an old-style or classic class. A revised
implementation of classes in Python came in version 2.2 with
new-style classes. The specification of a new-style class requires
(object)
after the class name:
class V(object):
...
New-style classes have more functionality, and it is in general recommended
to work with new-style classes.
We shall therefore from now write V(object)
rather than just V
.
In Python 3, all classes are new-style whether we write V
or V(object)
.
A function may have a doc string right after the
function definition, see the section ref{sec:basic:docstring}.
The aim of the doc string is to explain the purpose of the function
and, for instance, what the arguments and return values are.
A class can also have a doc string, it is just the first string that
appears right after the class
headline.
The convention is to enclose the doc string in triple double quotes """
:
class Y(object):
"""The vertical motion of a ball."""
def __init__(self, v0):
...
More comprehensive information can include the methods and how the
class is used in an interactive session:
class Y(object):
"""
Mathematical function for the vertical motion of a ball.
Methods:
constructor(v0): set initial velocity v0.
value(t): compute the height as function of t.
formula(): print out the formula for the height.
Data attributes:
v0: the initial velocity of the ball (time 0).
g: acceleration of gravity (fixed).
Usage:
>>> y = Y(3)
>>> position1 = y.value(0.1)
>>> position2 = y.value(0.3)
>>> print y.formula()
v0*t - 0.5*g*t**2; v0=3
"""
Now we will provide some more explanation of the self
parameter and
how the class methods work. Inside the constructor __init__
, the
argument self
is a variable holding the new instance to be
constructed. When we write
self.v0 = v0
self.g = 9.81
we define two new data attributes in this instance. The self
parameter
is invisibly returned to the calling code. We can imagine that Python
translates the syntax y = Y(3)
to a call written as
Y.__init__(y, 3)
Now, self
becomes the new instance y
we want to create, so when we
do self.v0 = v0
in the constructor, we actually assign v0
to
y.v0
. The prefix with Y.
illustrates how to reach a class method
with a syntax similar to reaching a function in a module (just like
math.exp
). If we prefix with Y.
, we need to explicitly feed in an
instance for the self
argument, like y
in the code line above, but
if we prefix with y.
(the instance name) the self
argument is
dropped in the syntax, and Python will automatically assign the y
instance to the self
argument. It is the latter "instance name
prefix" which we shall use when computing with
classes. (Y.__init__(y, 3)
will not work since y
is undefined and
supposed to be an Y
object. However, if we first create y = Y(2)
and then call Y.__init__(y, 3)
, the syntax works, and y.v0
is 3
after the call.)
Let us look at a call to the value
method to see a similar
use of the self
argument. When we write
value = y.value(0.1)
Python translates this to a call
value = Y.value(y, 0.1)
such that the self
argument in the value
method becomes
the y
instance. In the expression inside the value
method,
self.v0*t - 0.5*self.g*t**2
self
is y
so this is the same as
y.v0*t - 0.5*y.g*t**2
The use of self
may become more apparent when we have multiple class
instances. We can make a class that just has one parameter so we
can easily identify a class instance by printing the value of this
parameter. In addition, every Python object obj
has a unique
identifier obtained by id(obj)
that we can also print to track
what self
is.
class SelfExplorer(object):
def __init__(self, a):
self.a = a
print 'init: a=%g, id(self)=%d' % (self.a, id(self))
def value(self, x):
print 'value: a=%g, id(self)=%d' % (self.a, id(self))
return self.a*x
Here is an interactive session with this class:
>>> s1 = SelfExplorer(1)
init: a=1, id(self)=38085696
>>> id(s1)
38085696
We clearly see that self
inside the constructor is the same
object as s1
, which we want to create by calling the constructor.
A second object is made by
>>> s2 = SelfExplorer(2)
init: a=2, id(self)=38085192
>>> id(s2)
38085192
Now we can call the value
method using the standard syntax s1.value(x)
and the "more pedagogical" syntax SelfExplorer.value(s1, x)
.
Using both s1
and s2
illustrates how self
take on different
values, while we may look at the method SelfExplorer.value
as
a single function that just operates on different self
and x
objects:
>>> s1.value(4)
value: a=1, id(self)=38085696
4
>>> SelfExplorer.value(s1, 4)
value: a=1, id(self)=38085696
4
>>> s2.value(5)
value: a=2, id(self)=38085192
10
>>> SelfExplorer.value(s2, 5)
value: a=2, id(self)=38085192
10
Hopefully, these illustrations help to explain that self
is just
the instance used in the method call prefix, here s1
or s2
.
If not, patient work with class programming in Python will over
time reveal an understanding of what self
really is.
self
.
self
as first argument. (The name can be any valid variable name, but the name self
is a widely established convention in Python.)self
represents an (arbitrary) instance of the class.self
, as in self.name
, where name
is the name of the attribute.self
is dropped as argument in calls to class methods.
Let us apply the ideas from the Y
class to the function
$$
\begin{equation*}
v(r) = \left({\beta\over 2\mu_0}\right)^{{1/ n}}
{n \over n+1}\left( R^{1 + 1/n} - r^{1 + 1/n}\right) ,
\end{equation*}
$$
where \( r \) is the independent variable.
We may write this function as \( v(r; \beta,\mu_0,n,R) \) to
explicitly indicate that
there is one primary independent variable (\( r \)) and four physical
parameters \( \beta \), \( \mu_0 \), \( n \), and \( R \).
The class typically holds
the physical parameters as variables and provides an value(r)
method
for computing the \( v \) function:
class V(object):
def __init__(self, beta, mu0, n, R):
self.beta, self.mu0, self.n, self.R = beta, mu0, n, R
def value(self, r):
beta, mu0, n, R = self.beta, self.mu0, self.n, self.R
n = float(n) # ensure float divisions
v = (beta/(2.0*mu0))**(1/n)*(n/(n+1))*\
(R**(1+1/n) - r**(1+1/n))
return v
There is seemingly
one new thing here in that we initialize several variables on the
same line:
self.beta, self.mu0, self.n, self.R = beta, mu0, n, R
The comma-separated list of variables on the right-hand
side forms a tuple so this assignment is just the a valid construction where
a set of variables on the left-hand side is set equal to a list or tuple
on the right-hand side, element by element. An
equivalent multi-line code is
self.beta = beta
self.mu0 = mu0
self.n = n
self.R = R
In the value
method it is convenient to avoid the
self.
prefix in the mathematical formulas and instead introduce
the local short names beta
, mu0
, n
, and R
.
This is in general a good idea, because it makes it easier to read the
implementation of the formula and check its correctness.
Another solution to the problem of sending functions with parameters
to a general library function such as diff
is provided in
the document Variable number of
function arguments in Python
[2]. The remedy there is to transfer the
parameters as arguments "through" the diff
function. This can be
done in a general way as explained in that appendix.
To illustrate class programming further, we will now realize class
Y
from the section Representing a function as a class in a different way.
You may consider this section as advanced and skip it, but for
some readers the material might improve the understanding of
class Y
and give some insight into
class programming in general.
It is a good habit always to have a constructor in a class and to
initialize the data attributes in the class here, but this is not a
requirement. Let us drop the constructor and make v0
an optional
argument to the value
method. If the user does not provide v0
in
the call to value
, we use a v0
value that must have been provided
in an earlier call and stored as a data attribute self.v0
. We can
recognize if the user provides v0
as argument or not by using None
as default value for the keyword argument and then test if v0 is
None
.
Our alternative implementation of class Y
, named Y2
, now reads
class Y2(object):
def value(self, t, v0=None):
if v0 is not None:
self.v0 = v0
g = 9.81
return self.v0*t - 0.5*g*t**2
This time the class has only one method and one data attribute as we
skipped the constructor and let g
be a local variable in
the value
method.
But if there is no constructor, how is an instance created? Python fortunately creates an empty constructor. This allows us to write
y = Y2()
to make an instance y
. Since nothing happens in the automatically
generated empty constructor, y
has no data attributes at this stage.
Writing
print y.v0
therefore leads to the exception
AttributeError: Y2 instance has no attribute 'v0'
By calling
v = y.value(0.1, 5)
we create an attribute self.v0
inside the value
method.
In general, we can create any attribute name
in any method by just assigning
a value to self.name
. Now trying a
print y.v0
will print 5
.
In a new call,
v = y.value(0.2)
the previous v0
value (5
) is used inside value
as
self.v0
unless a v0
argument is specified in the call.
The previous implementation is not foolproof if we fail to initialize
v0
. For example, the code
y = Y2()
v = y.value(0.1)
will terminate in the value
method with the exception
AttributeError: Y2 instance has no attribute 'v0'
As usual, it is better to notify the user with a more informative message.
To check if we have an attribute v0
, we can use the Python
function hasattr
. Calling hasattr(self, 'v0')
returns
True
only if the instance self
has an attribute
with name 'v0'
. An improved value
method now reads
def value(self, t, v0=None):
if v0 is not None:
self.v0 = v0
if not hasattr(self, 'v0'):
print 'You cannot call value(t) without first '\
'calling value(t,v0) to set v0'
return None
g = 9.81
return self.v0*t - 0.5*g*t**2
Alternatively, we can try to
access self.v0
in a try-except
block, and
perhaps raise an exception TypeError
(which is what Python raises if
there are not enough arguments to a function or method):
def value(self, t, v0=None):
if v0 is not None:
self.v0 = v0
g = 9.81
try:
value = self.v0*t - 0.5*g*t**2
except AttributeError:
msg = 'You cannot call value(t) without first '
'calling value(t,v0) to set v0'
raise TypeError(msg)
return value
Note that Python detects an AttributeError
, but from a user's
point of view, not enough parameters were supplied in the call so
a TypeError
is more appropriate to communicate back to the
calling code.
We think class Y
is a better implementation than class Y2
,
because the former is simpler. As already mentioned, it
is a good habit to include
a constructor and set data here rather than "recording data on the fly"
as we try to in class Y2
. The whole purpose of class Y2
is just to show that Python provides great flexibility with respect
to defining attributes, and that there are no requirements to what
a class must contain.
Newcomers to the class concept often have a hard time understanding what this concept is about. The present section tries to explain in more detail how we can introduce classes without having the class construct in the computer language. This information may or may not increase your understanding of classes. If not, programming with classes will definitely increase your understanding with time, so there is no reason to worry. In fact, you may safely jump to the section Special methods as there are no important concepts in this section that later sections build upon.
A class contains a collection of variables (data) and a collection of methods (functions). The collection of variables is unique to each instance of the class. That is, if we make ten instances, each of them has its own set of variables. These variables can be thought of as a dictionary with keys equal to the variable names. Each instance then has its own dictionary, and we may roughly view the instance as this dictionary. (The instance can also contain static data attributes (the section Static methods and attributes), but these are to be viewed as global variables in the present context.)
On the other hand, the methods are shared
among the instances. We may think of a method in a class
as a standard global
function that takes an instance in the form of a dictionary
as first
argument. The method has then access to the variables in the instance
(dictionary) provided in the call.
For the Y
class from the section Representing a function as a class
and an instance y
,
the methods are ordinary
functions with the following names and arguments:
Y.value(y, t)
Y.formula(y)
The class acts as a namespace,
meaning that all functions
must be prefixed by the namespace name, here Y
.
Two different classes, say C1
and C2
, may have functions
with the same name, say value
, but when the value
functions
belong to different namespaces, their names C1.value
and
C2.value
become distinct.
Modules are also namespaces for the functions and variables in them
(think of math.sin
, cmath.sin
, numpy.sin
).
The only peculiar thing with the class construct in Python is that it allows us to use an alternative syntax for method calls:
y.value(t)
y.formula()
This syntax coincides with the traditional syntax of calling class
methods and providing arguments, as found in other computer languages,
such as Java, C#, C++, Simula, and Smalltalk.
The dot notation is also used to access variables in an instance
such that we inside a method can write self.v0
instead of
self['v0']
(self
refers to y
through the function call).
We could easily
implement a simple version of the class concept without having a class
construction in the language. All we need is a dictionary type and
ordinary functions. The dictionary acts as the instance, and methods are
functions that take this dictionary as the first argument such that
the function has access to
all the variables in the instance.
Our Y
class could now be implemented as
def value(self, t):
return self['v0']*t - 0.5*self['g']*t**2
def formula(self):
print 'v0*t - 0.5*g*t**2; v0=%g' % self['v0']
The two functions are placed in a module called Y
.
The usage goes as follows:
import Y
y = {'v0': 4, 'g': 9.81} # make an "instance"
y1 = Y.value(y, t)
We have no constructor since the initialization of the variables is
done when declaring the dictionary y
, but we could well include
some initialization function in the Y
module
def init(v0):
return {'v0': v0, 'g': 9.81}
The usage is now slightly different:
import Y
y = Y.init(4) # make an "instance"
y1 = Y.value(y, t)
This way of implementing classes with the aid of a dictionary and
a set of ordinary functions actually forms the basis for class implementations
in many languages. Python and Perl even
have a syntax that demonstrates this type of
implementation. In fact, every class instance in Python has a dictionary
__dict__
as attribute, which holds all the
variables in the instance. Here is a demo that proves the existence of this
dictionary in class Y
:
>>> y = Y(1.2)
>>> print y.__dict__
{'v0': 1.2, 'g': 9.8100000000000005}
To summarize: A Python class can be thought of as some variables collected in a dictionary, and a set of functions where this dictionary is automatically provided as first argument such that functions always have full access to the class variables.
We have in this section provided a view of classes from a technical point of view. Others may view a class as a way of modeling the world in terms of data and operations on data. However, in sciences that employ the language of mathematics, the modeling of the world is usually done by mathematics, and the mathematical structures provide understanding of the problem and structure of programs. When appropriate, mathematical structures can conveniently be mapped on to classes in programs to make the software simpler and more flexible.
The view of classes in this section neglects very important topics such as inheritance and dynamic binding (explained in the document Object-oriented programming [1]). For more completeness of the present section, we therefore briefly describe how our combination of dictionaries and global functions can deal with inheritance and dynamic binding (but this will not make sense unless you know what inheritance is).
Data inheritance can be obtained by letting a subclass dictionary
do an update
call with the superclass dictionary as argument.
In this way all data in the superclass are also available in the
subclass dictionary. Dynamic binding of methods is more complicated, but
one can think of checking if the method is in the subclass module
(using hasattr
), and if not, one proceeds with checking super
class modules until a version of the method is found.
This section follows up the discussion in the section Making classes without the class construct and presents a more advanced construction that may serve as alternative to class constructions in some cases.
Our motivating example is that we want a Python implementation of a mathematical function \( y(t;v_0)=v_0t - \frac{1}{2}gt^2 \) to have \( t \) as the only argument, but also have access to the parameter \( v_0 \). Consider the following function, which returns a function:
>>> def generate_y():
... v0 = 5
... g = 9.81
... def y(t):
... return v0*t - 0.5*g*t**2
... return y
...
>>> y = generate_y()
>>> y(1)
0.09499999999999975
The remarkable property of the y
function is that it remembers the
value of v0
and g
, although these variables are not local to
the parent function generate_y
and not local in y
. In particular,
we can specify v0
as argument to generate_y
:
>>> def generate_y(v0):
... g = 9.81
... def y(t):
... return v0*t - 0.5*g*t**2
... return y
...
>>> y1 = generate_y(v0=1)
>>> y2 = generate_y(v0=5)
>>> y1(1)
-3.9050000000000002
>>> y2(1)
0.09499999999999975
Here, y1(t)
has access to v0=1
while y2(t)
has access to v0=5
.
The function y(t)
we construct and return from generate_y
is
called a closure and it remembers the value of the surrounding
local variables in the parent function (at the time we create the
y
function). Closures are very convenient for many
purposes in mathematical computing.
Examples appear in the section Example: Automagic differentiation.
Closures are also central in
a programming style called functional programming.
As soon as you get the idea of a closure, you will probably use it a lot because it is a convenient way to pack a function with extra data. However, there are some pitfalls. The biggest is illustrated below, but this is considered advanced material!
Let us generate a series of functions v(t)
for various
values of a parameter v0
. Each function just returns
a tuple (v0, t)
such that we can easily see what the argument and
the parameter are. We use lambda
to quickly define
each function, and we place the functions in a list:
>>> def generate():
... return [lambda t: (v0, t) for v0 in [0, 1, 5, 10]]
...
>>> funcs = generate()
Now, funcs
is a list of functions with one argument.
Calling each function and printing
the return values v0
and t
gives
>>> for func in funcs:
... print func(1)
...
(10, 1)
(10, 1)
(10, 1)
(10, 1)
As we see, all functions have v0=10
, i.e., they stored the most recent
value of v0
before return. This is not what we wanted.
The trick is to let v0
be a keyword argument in each function,
because the value of a keyword argument is frozen at the time the
function is defined:
>>> def generate():
... return [lambda t, v0=v0: (v0, t)
... for v0 in [0, 1, 5, 10]]
...
>>> funcs = generate()
>>> for func in funcs:
... print func(1)
...
(0, 1)
(1, 1)
(5, 1)
(10, 1)