Associate Professor Justin Dressel
Faculty of Mathematics, Physics, and Computation
Schmid College of Science and Technology
Modeling
Processing
Visualization
Mathematical objects
Physical states
CS types/structures
Transformations/Morphisms
Evolution/state updates
Algorithms/programs
Plots/graphs/movies/arrays
Scientific problems are data-centric
Organizing data and its possible transformations is essential
Problem
(why)
Interface (what)
Test Cases (check)
Implementation (how)
Each small problem is an encapsulated module
To answer the main question, many modules must work together
Changing the implementation should not affect the interface
"Object-oriented"
"Functional"
"Logical"
"Procedural"
(Others: concurrent, actor-based, data-flow, rule-based, symbolic, etc.)
Increasing abstraction away from hardware:
increases portability, simplifies coding, decreases efficiency
High-level
Low-level
Rule of thumb
Order of Importance:
Low-level code is difficult to write correctly, and scientific problems are often hard to solve
A more effective strategy: solve problem with high-level language, then...
The first two steps toward modular design are the most important in Python:
Functions should do one thing well
Functions should choose one:
Well-written functions can be tested individually, with unit tests
Modules can be imported, and their functions can be reused
Promote reuse of code. Your code is extending the Python language.
#!/usr/bin/env python
import functools as ft
import operator as op
"""Example Module factorial.py
This is the module docstring.
"""
def fac(x):
"""The factorial of x: x!
This is product of all positive integers less
than or including x.
Parameters:
-----------
x : int > 0
Integer argument for factorial
Returns:
--------
fib : int
"""
return ft.reduce(op.mul, range(1, x+1), 1)
def test_fac5_120():
"""Test function for factorial.
Test case: 5! == 120
"""
assert fac(5) == 120, "5! == "+str(fac(5))
The next step toward modular design in Python is to understand classes and objects
At its core, Python is an object-oriented language
This means we define new types of data (or classes),
which have specific instances of data (or objects)
that possess their own functions (or methods)
Classes are useful in two cases:
Consider this example code for a class of Bear objects:
class Bear(object): # subclass of "object"
"""Bear as a data-type"""
fur = "brown" # class attribute
# (data stored by class)
def __init__(self, name): # class constructor
self.name = name # instance attribute
# (data stored by object)
def roar(self): # instance method
print("I, {}, do roar.".format(self.name))
Run this code one line at a time: what does it do?
Note: The last three lines are identical in meaning.
Python converts the first to the last automatically.
This is why the method roar has first argument self : when executing b1.roar(), Python passes the function Bear.roar the object b1 as the argument self.
Automatic syntax conversion like this is called "syntactic sugar" - it's a convenience to save typing.
Bear.fur
b1 = Bear("Ronaldo")
type(b1)
b1.name
b1.fur
b2 = Bear("Napoleon")
b2.name
b2.roar()
b1.roar()
Bear.roar(b1)
type(b1).roar(b1)
A class is mostly a fancy way of organizing namespaces by type.
__init__ is a "constructor" method that is run only once, when the object is created. The arguments of __init__ are the ones used to construct the object and set default properties.
A class defines a new type. This type may extend an existing type, called "subclassing" a type. In this case, we are subclassing the most generic type "object."
Here is an illustrative example of how one can use a class to store an already computed result to prevent redundant calculation: this is called memoizing
#!/usr/bin/env python
import functools as ft
import operator as op
"""Example Module file factorial.py
This is the module docstring.
"""
class Factorial(object):
"""Memoized version of the factorial of x: x!
This stores the product of all positive integers less
than or including x when called.
Attributes:
-----------
data : {int : int}
Stored results of factorial
"""
def __init__(self):
"""Constructor for Factorial of x"""
self.data = dict()
def __call__(self, x):
"""Compute factorial of x"""
assert (type(x)==int and x >= 0), "Not a positive integer"
if x not in self.data.keys():
self.data[x] = ft.reduce(op.mul, range(1,x+1), 1)
return self.data[x]
fac = Factorial() # replaces previous function definition
def test_fac5_120(): # This test is unaltered : interface preserved!
"""Test function for factorial.
Test case: 5! == 120
"""
assert fac(5) == 120, "5! == "+str(fac(5))
In [1]: import factorial as fac
In [2]: f = fac.Factorial()
# This runs fac.Factorial.__init__(f)
In [3]: f.data
{} # Nothing is stored in output yet
In [4]: f(5)
# This runs fac.Factorial.__call__(f, 5)
Out[4]: 120
In [5]: f.data
# Now the output has been stored for future use
Out[5]: {5 : 120}
Basic ideas:
Suppose we want to create variations of a type, while keeping most things the same.
This is known as subclassing, and is a fancy way of just copying and modifying another class without rewriting everything. (Note that self.name is not defined here!)
This creates a tree hierarchy : object => class => subclass1 => ...
We say that a subclass inherits the properties further up the hierarchy.
All objects in Python eventually subclass the most basic type: object.
class StuffedBear(Bear):
def roar(self):
print("I, {}, cannot roar.".format(self.name))
This extends the Bear class by modifying the roar method.
b = Bear("Ronaldo")
s = StuffedBear("George")
b.roar()
s.roar()
super(StuffedBear, s).roar()
What does this code do?
Note the function super retrieves the original ("parent") class instead. This is especially useful when modifying the __init__ method.
Methods surrounded by underscores are special, and hidden from tab-complete
(use dir(instance) to see all hidden methods)
Examples:
In [1]: class Gen(object):
.....: def __init__(self, max):
.....: self.val = 1
.....: self.max = max
.....: def __iter__(self):
.....: return self
.....: def __next__(self):
.....: prev = self.val
.....: self.val += 2
.....: if prev > self.max:
.....: raise StopIteration
.....: else:
.....: return prev
.....: def __call__(self):
.....: return self.__next__()
.....:
In [2]: g = Gen(10)
In [3]: g()
Out[3]: 1
In [3]: [i for i in g]
Out[3]: [3, 5, 7, 9]
Idea: Everything in Python follows apparent structure
If it looks like a duck and quacks like a duck, it is a duck.
Magic methods are conventions used by the language to identify common structural features and use them
Practice makes perfect.
Keep references handy until you remember commands on command.