Scientific Computing Using Python - PHYS:4905
Lecture Notes #25 - Prof. Kaaret
Procedural programming
All of the code that we have written to date uses a paradigm called
procedural programming. One defines some data structures and
then writes procedures that act on those structures. The basic
idea is illustrated below. In the main body of the program, we
create some global variables that hold the data structures we wish
to use. These global variables can be accessed either via
lines of code in the program main body or by code in functions that
we define.

Critical aspects are:
- any function may access any global variable, and
- all of the functions need to agree on how the data structures in
the global variables are defined.
The latter means that if you change how a data structure in a global
variable is defined, then you need to change all of the functions
that access that variable. The former means that it is
possible to have unintended side effects. If you inadvertently
define two functions that access a global variable with the same
name, but with different intent, then calling one function may
causes errors when calling the other. (This sort of
programming error is often difficult to debug.)
Note that the functions may have local variables. These are
variables that can be accessed only within the function. Also,
the variables only persist as long as the function is active.
If you call Function A that defines some local variables, then those
local variables are lost when then functions returns.
Object oriented programming
It is possible to write code using a different paradigm called
object oriented programming. In object oriented programming
one encapsulates the data with the code. The basic idea
is illustrated below. In the main body of the program, we
create some objects. An object contains both object
variables that hold the data structures we wish to use and methods
that act on the object variables.
Object variables are fundamentally different from local variables
within a function in that they persist for as long as the object
itself persists. One instantiates or creates an object
at which time the object variables are created and
initialized. Then one calls the object's methods to change the
state of the object or, equivalently, to perform computations on the
object variables. Think of methods as functions that are
specific to an object. Methods can act only on the variables
of the object in which they are defined and cannot be used to act on
the variables other objects. The object variables persist
between successive method calls.

Critical aspects are:
- we no longer use global variables, and
- an object's variables can be accessed only by the methods in that
object.
The fact that the variables and methods are grouped together inside
objects and that an object's variables can be accessed only by the
methods in an object is called encapsulation. One nice
result of encapsulation is that it is not possible to have two
different objects access a global variable with the same name, but
with different intent. This eliminates the potential for
errors that arise from using the same variable name in different
objects. Encapsulation keeps control of an object's data
structures inside the methods that define the object - you don't
have to worry about someone coming along and messing with your
variables. This makes code cleaner and easier to debug,
particularly for large and complex programs.
Object Oriented EROS or OOEROS
This discussion is a bit abstract, so let's look at some Python
code. We'll translate the elementary row operations (EROS)
that we wrote to do Gaussian elimination into object oriented
Python.
The first thing that we do is define a class. We use
the Python keyword class and the syntax looks like a function
definition with no arguments. Keeping with the idea of objects
rather than procedures, we call our class augmentedMatrix.
The first method that we define in our class is the initialization
method which must be called __init__. This method is
called every time we create a new object or instance from
our class. The first argument of our initialization method is
self. This will be the first argument of all the methods and
gives the methods a way to access the data structures associated
with the object. The second argument is the initial value that
we wish to assign to our augmented matrix. The method takes
those values, converts them to an array (just in case they don't
start that way), and stores the values in the variable m
that is part of self.
class augmentedMatrix :
"""
Define a class to perform elementary row operations on
augmented matrices.
The augmented matrix is stored as a object variable.
The three standard EROS are defined as methods along with
methods to
print and return the augmented matrix.
"""
def __init__(self, initialMatrix) :
# store the initial matrix into the object
variable m
self.m = np.array(initialMatrix)
To create an object using this class, we use a line like the
one below.
m = augmentedMatrix([[1.0, 1.0, 27],[2, -1, 0]]) # define
augmented matrix
It looks like a function call, but since we defined augmentedMatrix
as a class instead of as a function, it creates a new object that we
then assign to the variable m. Note that Python
handles objects just as it does any other data type (such as float
or list), for example you can make lists of objects. If you
recall the early lecture where I said that all variables in Python
are objects, this starts to make sense.
Now let's look at the row addition method.
def row_add(self, i, j, c) :
# multiply row i by c, add to row j, and
put sum back into row j
self.m[j,:] = c*self.m[i,:]+self.m[j,:]
The first argument, as always for classes, is self. The other
arguments are the same as we had in the procedural case. The
row i that gets added after being multiplied by the scalar
c, and the other row j in the addition where the
sum ends up.
We use self to refer to data stored inside the object. In this
case, self.m is the augmented matrix. The line of
code that does the computation is very similar to the corresponding
line of code in the row_add function from the procedural code, but
with self in front of all of the variables. For comparison,
that code is:
def row_add(m, i, j, c) :
"""
Multiply row i of matrix m by c, add to row j, and replace
row j.
"""
mp = m.copy() # make a copy of the input matrix
mp[j,:] = c*mp[i,:]+mp[j,:] # multiply row i by c, add to
row j, and put sum back into row j
return mp
To call the row addition method, we use a line like
m.row_add(1, 0, 0.5)
while in the procedural code, we the corresponding line would be
m = row_add(m, 1, 0, 0.5)
Note the differences in the calls and in the function versus
method. In the procedural code, the function takes the matrix
as an argument, explicitly makes a copy of it so as to not change
the original matrix, and then returns the matrix. It's up to
the user to keep track of the matrix. In the object oriented
version, the matrix stays internal to the object. The user
does not pass the matrix as an argument and the method does not
return the matrix. In fact, the user doesn't have access to
the matrix unless we write a method to allow that.
Object Oriented Gaussian Elimination
Now let's redo homework problem 6-1 using object oriented Python
code. Download augmentedMatrix.py
to your machine and load it into Spyder. Trying running it.
The first part of the code defines the augmentedMatrix class.
- __init__ is used to initialize the matrix when a new
augmentedMatrix object is created.
- There is a method for each elementary row operation. Note that
these act on the object variable self.m. There is no need
to pass them a matrix and they don't return a matrix. The
data is kept inside the object.
- The print method lets you print the matrix data inside the
object.
The second part of the code uses the augmentedMatrix class to
perform Gaussian elimination on the example from class. The
line
m = augmentedMatrix([[1.0, 1.0, 27],[2, -1, 0]])
creates an object of the augmentedMatrix class with the
matrix initialized to the starting matrix.
The lines
m.row_swap(0, 1)
m.scalar_mult(0, 0.5)
m.row_add(0, 1, -1)
m.scalar_mult(1, 2.0/3.0)
m.row_add(1, 0, 0.5)
perform the elementary row operations. Note (yet again) that
the methods act on data hidden inside the object. You don't
have to pass the matrix back and forth.
To access the data, we use
m.print()
which displays the augmented matrix.
Inheritance
It's possible to extent class definitions. The new class inherits
all of the methods defined in the base class and you can add and/or
replace methods as you see fit. For example, say you wanted to
add a method to the augmentedMatrix class that gives you access to
the matrix so that you can use it in other calculations. You
could define a new class using
class derivedAugmentedMatrix(augmentedMatrix) :
Now the class definition takes an argument, which is another class
that has already been defined. If you do nothing else, the new
class will be a copy of the old one. But you can add method
definitions. If the new methods have the same name as methods
in the original class, then they will replace the original
methods. If the names are different, then your code adds new
methods. All of the original methods that are not replaced are
available in the new class. This is call inheritance.
You can actually make a new class that inherits from several
different classes by listing more than one class in the argument in
the class statement. This is called multiple inheritance.
Let's add the following lines to our Python code after the
definition of the augmentedMatrix class.
class derivedAugmentedMatrix(augmentedMatrix) :
def matrix(self) :
# return the matrix
# we need this to gain access to the
matrix stored as an object variable
return self.m
Then change the line
m = augmentedMatrix([[1.0, 1.0, 27],[2, -1, 0]]) # define
augmented matrix
to be
m = derviedAugmentedMatrix([[1.0, 1.0, 27],[2, -1, 0]]) #
define augmented matrix
And at the end of the
program add the line
print(2*m.matrix())
Now run the program.
The last action of the program should be to print the matrix
multiplied by 2. You have now learned to do object oriented
programming in Python including using inheritance to extend class
definitions. Isn't that nice?