Object-Oriented Python
Overview
Associate Professor Justin Dressel
Faculty of Mathematics, Physics, and Computation
Schmid College of Science and Technology
Trichotomy of Data
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
Scientific Problem Solving
- Identify a concrete question
- Identify the data to be modeled/processed/visualized
- Decompose the question into smaller problems
- Design unit tests to check each small problem in isolation
- Implement solutions, ensuring all unit tests pass correctly
- Compose solutions together to answer main question
- Check correctness of solutions with end-to-end tests
- Profile performance of solution
-
Optimize:
- rethink data structures (2.)
- refactor common code (3.)
- redesign efficient algorithms (5.)
- reimplement slow parts as faster modules (5.)
- retest to catch and prevent bugs (7.)
Problem
(why)
Interface (what)
Test Cases (check)
Implementation (how)
- Data structure choice
- Algorithm choice
- Language choice
- Other optimizations
Modular Design
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
Language Paradigms
"Object-oriented"
"Functional"
"Logical"
- Structure primary (classes/objects)
- Functions belong to data (object methods)
- Data arranged in hierarchy (inheritance)
- Examples:
C++, Java, Python, Ruby, Julia, Rust
- Process primary (functions)
- Data belong to functions (closures)
- Functions arranged in hierarchy (closures)
-
Examples:
Mathematica, Python, Haskell, Julia, R
- Constraints primary (rules/clauses)
- Data and functions belong to clauses
- Hierarchical clauses become unified
-
Examples:
Prolog, Mercury, Curry,
Python (LogPy, Pyke)
"Procedural"
- Instructions primary, limited types of data
- Examples: C, MATLAB, Python, Julia, Go, Rust
(Others: concurrent, actor-based, data-flow, rule-based, symbolic, etc.)
Language Abstraction Levels
-
Hardware level (Binary, Machine code, Assembly)
Code executed by the hardware processor directly as procedural instructions -
Low-level native compilation ( C, Go, Rust)
Cross-architecture abstraction, compiled to machine code before execution -
High-level native compilation ( C++, Haskell, LISP)
Sophisticated and expressive abstraction, still compiles to machine code first -
Just-In-Time (JIT) compilation ( Julia, Java, MATLAB)
Cross-platform compiled code, converted to machine code during execution -
Interpreted bytecode ( Python, R, Mathematica)
Code read and executed by an interpreter, which can use compiled libraries
Increasing abstraction away from hardware:
increases portability, simplifies coding, decreases efficiency
High-level
Low-level
Rule of thumb
BUT: Efficiency is multi-faceted
- Rethink efficient algorithms and data structures
- Exploit modular design
- solve problems at a high level, but use fast low-level libraries for speed
- Profile code, refactor, and reimplement libraries
- isolate slow parts, and replace just those with faster low-level libraries
- Parallelize code
- run independent pieces of code on many processors (or GPUs)
- Reimplement entire correct solution (rapid prototyping):
- Replace interpreters with Just-In-Time (JIT) compilers (e.g., JVM, .NET)
- Replace JIT with high-level native code-compilation (e.g., LLVM, C++)
- Abandon high-level code for low-level native compilation (e.g., C, Rust)
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...
Modular Design in Python
The first two steps toward modular design are the most important in Python:
- Organize code into functions
- Organize functions into (python) modules
Functions should do one thing well
Functions should choose one:
- Return a value
- Perform an action
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))
Object-Oriented Design
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)
Why Use Classes?
Classes are useful in two cases:
- You wish to organize both functions and related data into logical groupings such that they remain tightly bound together
- You wish to incrementally add functionality by extending a base case into more elaborate variations of that base case
Class Breakdown
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."
Example Class for Factorial
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:
- a class describes similar objects
- an object is an instance of a class
- data inside an object are attributes
- functions inside an object are methods
- the special parameter self describes one particular instance of the class
- the special method __init__ constructs new instances of the class
- the special method __call__ executes an instance of the class as a normal function
Extending Objects
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.
Magic Methods
Methods surrounded by underscores are special, and hidden from tab-complete
(use dir(instance) to see all hidden methods)
Examples:
-
__init__(self, *args, **kwargs)
Constructor for a class -
__call__(self, *args, **kwargs)
Run if instance f is executed: f(*args, **kwargs) -
__iter__(self)
Required to indicate object is an iterator -
__next__(self)
Run for an iterable to retrieve the next iteration
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
Further Reading
Practice makes perfect.
Keep references handy until you remember commands on command.
Python Objects Overview
By Justin Dressel
Python Objects Overview
An introduction to Python objects as a method for modular design in scientific computing.
- 4,521