Object-Oriented Programming in Python
The subject is large and complex- We will focus on the basics
- The rest is up to you
At first glance the OOP techniques look complicated
But once you get used to it, usage is pretty simple
I talk a lot about how things work
- Don't worry if you can't understand it all first time round
- Just try to memorize the rules
Overview
Python supports both procedural and object-oriented programming (OOP)- In contrast
- C is a procedural language
- C++ is is C with OOP
- Fortran and MATLAB are mainly procedural
- Some OOP has been tacked on recently
- JAVA is (relatively) pure OOP
But OOP has become a central part of modern program design because
- Design pattern fits well with the human brain
- Facilitates clean, efficient code (when used well)
Key Concepts
The procedural programming paradigm: based around functions (procedures)- The program has a state, which is the values of its variables
- Functions are called to act on this data according to the task
- Data is passed backwards and forwards via function calls
Example
- A list that stores numbers
- the numbers are the data
- And knows how to sort itself
- the sorting function is part of the object
- Describes what kind of data it stores
- What functions it has for acting on this data
- Functions defined within classes are referred to as methods
- Created from the blueprint (i.e., class definition)
- Typically has its own unique data
- Use the methods defined in the class to act on that data
- The data and methods of an object are collectively referred to as attributes
- Accessed as
object.data
orobject.method_name()
- Accessed as
- Each instance object has it's own namespace to store its data
{'data': 42}
Examples
Let's try to clarify this with some examplesExample 1: Aquarium
Suppose we are developing an aquarium screensaverContains
- Fish
- Crabs
- Seaweed
- Bubbles, etc.
Class definitions specify what data and behavior (methods) they will have
Here's the psuedocode for a some class definitions
- definition of Fish class
class Fish:
data:
size
color
location
methods:
swim_forwards
turn_left
turn_right
eat_fish_food
- definition of Crab class
class Crab:
data:
size
color
location
methods:
crawl_left
crawl_right
move_claws
- These are the objects
- Small orange crab bottom left
- Big blue fish in the middle
- Small yellow fish top right
crab1 = Crab(small, orange, bottom_left) # Create a Crab, passing instance data
fish1 = Fish(big, blue, middle) # Create a Fish, passing instance data
fish2 = Fish(small, yellow, top_right) # Create a Fish, passing instance data
fish1.swim_forwards()
fish2.eat_fish_food()
crab1.move_claws()
- Swimming and crawling change the location
Example 2: Built-in ADTs
We have met several built-in typesLet's look at the case of lists
Given
X = [1, 2]
, interpreter creates an instance of a list- Stores the data
[1, 2]
in memory - Recorded as an object of type list.
- The identifier
X
is bound to this object
X.reverse()
changes the data to[2, 1]
X.append('foo')
changes it to[2, 1, 'foo']
X[i]
is a method call: equivalent to X.__getitem__(i)
>>> X = ['a', 'b']
>>> X[0]
'a'
>>> X.__getitem__(0)
'a'
X
are things that can be accessed via X.attribute_name
The
dir()
function returns the attribute names as a list:>>> dir(X)
['__add__', '__class__', '__doc__', ..., 'pop', 'remove', 'reverse', 'sort']
User-Defined ADTs
With Python, it is simple to build our own ADTsExample: Dice
Let's build a class to represent diceData
- The current state: the side facing up
- Roll: changes the state (side facing up)
class Dice:
data:
dots -- the side facing up (i.e., number of dots showing)
methods:
roll -- roll the dice (i.e., change dots)
import random
class Dice:
def __init__(self, dots):
self.dots = dots
def roll(self):
self.dots = random.choice((1, 2, 3, 4, 5, 6))
self
is explained belowThe class has two methods:
__init__
and roll
The
__init__
method is a constructor- Used to create instances (objects) from the class definition with their own data
roll
method rolls the dice, changing the state (data) of a particular instanceClass Objects
Let's run it:john@c246:~$ python -i dice.py
>>> Dice
<class '__main__.Dice'>
- A place where all the definitions in the class are stored
Dice.__dict__
where some names are bound>>> print Dice.__dict__
{'__module__': '__main__',..., 'roll': <function roll at 0xb7cf4a3c>, '__init__': <function __init__ at 0xb7cf47d4>}
>>> Dice.__init__ # This is the constructor method that we defined
<unbound method Dice.__init__>
>>> Dice.roll # And the roll method that we defined
<unbound method Dice.roll>
Instance Objects
Now let's create an instance of the class- Done by calling the class name as if it were a function
>>> d1 = Dice(6) # Create an instance, passing 6 to the instance variable dots
>>> d1
<__main__.Dice object at 0xb7da348c>
>>> d1.dots # Accessing the instance data
6
>>> d1.__dict__ # Let's have a look at the namespace of d1
{'dots': 6}
>>> d1.roll()
>>> d1.dots
3
dots
was changed by the call to roll()
Let's create some more instances
>>> d2 = Dice(3)
>>> d3 = Dice(1)
>>> d2.dots
3
>>> d3.dots
1
>>> d3.roll()
>>> d3.dots
4
How does it work?
Here's the code for
roll()
def roll(self):
self.dots = random.choice((1, 2, 3, 4, 5, 6))
d1.roll()
is equivalent to (an abbreviation for) the call Dice.roll(d1)
- Calling method
roll()
defined in class objectDice
with instanced1
as the argument
roll()
executes, self
is bound to d1
In this way,
self.dots = random.choice((1, 2, 3, 4, 5, 6))
affects d1.dots
, which is what we wantThe formal rules for using
self
are (don't worry if you can't remember them all now):- Methods in the class definition must have
self
as the first argumentdef roll(self)
, etc.
- Instance attributes referenced inside the class definition must have the
self
prefixself.dots = random.choice((1, 2, 3, 4, 5, 6))
, etc.
- When calling a method attribute,
self
is passed implicitlyd1.roll()
, notd1.roll(d1)
Example: The Quadratic Map
The quadratic map difference equation is given byLet's write a class for generating time series
class QuadMap:
def __init__(self, initial_state):
self.x = initial_state
def update(self):
"Apply the quadratic map to update the state."
self.x = 4 * self.x * (1 - self.x)
def generate_series(self, n):
"""
Generate and return a trajectory of length n, starting at the
current state.
"""
trajectory = []
for i in range(n):
trajectory.append(self.x)
self.update()
return trajectory
self.update()
- Not
self.update(self)
, becauseself
is passed implicitly - Not just
update()
- Because we are referencing an attribute of the instance
self
that was passed in togenerate_series()
as a parameter
- Because we are referencing an attribute of the instance
>>> q = QuadMap(0.2)
>>> q.x
0.20000000000000001
>>> q.update() # Equivalent to QuadMap.update(q)
>>> q.x
0.64000000000000012
>>> q.generate_series(5) # The *second* parameter (i.e., n) in the method definition is bound to 5
[0.64000000000000012, 0.92159999999999986, 0.28901376000000045, 0.82193922612265036, 0.58542053873419597]
>>> q.x = 0.4 # Reset the state to 0.4
>>> q.generate_series(5)
[0.40000000000000002, 0.95999999999999996, 0.15360000000000013, 0.52002816000000029, 0.9983954912280576]
Example: A Class for Polynomials
Let's build a simple class to represent and manipulate polynomial functions.Data is the coefficients, which define a unique polynomial
Two methods
- Evaluate the polynomial, returning p(x) for any x
- Differentiate the polynomial, replacing original coefficients with those of p'
## Filename: polyclass.py
## Author: John Stachurski
class Polynomial:
def __init__(self, coefficients):
"""
Creates an instance of the Polynomial class representing
p(x) = a_0 x^0 + ... + a_N x^N, where a_i = coefficients[i].
"""
self.coefficients = coefficients
def evaluate(self, x):
y = 0
for i, a in enumerate(self.coefficients):
y += a * x**i
return y
def differentiate(self):
new_coefficients = []
for i, a in enumerate(self.coefficients):
new_coefficients.append(i * a)
# Remove the first element, which is zero
del new_coefficients[0]
# And reset coefficients data to new values
self.coefficients = new_coefficients
>>> from polyclass import Polynomial
>>> data = [2, 1, 3]
>>> p = Polynomial(data) # create instance of Polynomial
>>> p.evaluate(1)
6
>>> p.coefficients
[2, 1, 3]
>>> p.differentiate() # Modifies coefficients of p
>>> p.coefficients
[1, 6]
>>> p.evaluate(1)
7
evaluate
with __call__
then>>> p.__call__(1) == p(1)
Exercise
Recall the empirical distribution functionThe fraction of the sample which falls below x
Glivenko--Cantelli Theorem: converges to the true distribution function F
Implement as a class, where
- A given sample is the data for an instance
- The
__call__
method evaluates Fn(x) for any x
>>> from random import uniform
>>> samples = [uniform(0, 1) for i in range(10)]
>>> F = ecdf(samples)
>>> F(0.5)
>>> 0.29
>>> F.observations = [uniform(0, 1) for i in range(1000)]
>>> F(0.5)
>>> 0.479
Solution
## Filename: ecdf.py
## Author: John Stachurski
class ECDF:
def __init__(self, observations):
self.observations = observations
def __call__(self, x):
counter = 0.0
for obs in self.observations:
if obs <= x:
counter += 1
return counter / len(self.observations)
0 comments:
Post a Comment