Friday, December 23, 2011

Computational economics lecture 16


Object-Oriented Programming in Python

The subject is large and complex
  • We will focus on the basics
  • The rest is up to you
Take a deep breath, because this lecture is not easy
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
Any task can be accomplished using the older procedural style
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)
Fits well with mathematics because it encourages abstraction

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
The OOP paradigm: data and functions bundled together into abstract data types (ADTs)
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
class definition is a blueprint for an ADT
  • 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
An object or instance is one realization of the ADT
  • 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
In Python
  • The data and methods of an object are collectively referred to as attributes
    • Accessed as object.data or object.method_name()
  • Each instance object has it's own namespace to store its data
    • {'data': 42}

Examples

Let's try to clarify this with some examples

Example 1: Aquarium

Suppose we are developing an aquarium screensaver
Contains
  • Fish
  • Crabs
  • Seaweed
  • Bubbles, etc.
All of these can be represented in the program as objects
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
When the program starts, we instantiate (create instances of) fish, crabs, etc. from these definitions
  • These are the objects
Note that different instances can have different data
  • Small orange crab bottom left
  • Big blue fish in the middle
  • Small yellow fish top right
Here's the psuedocode for instantiation:
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
Now the main loop calls the methods of the objects to make them swim, crawl, eat, etc.
fish1.swim_forwards()
fish2.eat_fish_food()
crab1.move_claws()
Note that these methods may affect the internal data of the object
  • Swimming and crawling change the location

Example 2: Built-in ADTs

We have met several built-in types
Let'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
We know some methods that can act on this data
  • X.reverse() changes the data to [2, 1]
  • X.append('foo') changes it to [2, 1, 'foo']
In fact X[i] is a method call: equivalent to X.__getitem__(i)
>>> X = ['a', 'b']
>>> X[0]
'a'
>>> X.__getitem__(0)
'a'
The attributes of 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 ADTs

Example: Dice

Let's build a class to represent dice
Data
  • The current state: the side facing up
Method(s)
  • Roll: changes the state (side facing up)
Psuedocode:
class Dice:

    data:
        dots -- the side facing up (i.e., number of dots showing)

    methods:
        roll -- roll the dice (i.e., change dots)
Here's the Python code, in file dice.py
import random

class Dice:

    def __init__(self, dots):
        self.dots = dots 

    def roll(self):
        self.dots = random.choice((1, 2, 3, 4, 5, 6)) 
The use of self is explained below
The 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
The roll method rolls the dice, changing the state (data) of a particular instance

Class Objects

Let's run it:
john@c246:~$ python -i dice.py 
>>> Dice
<class '__main__.Dice'>
At this stage, the class definition has been compiled into a class object
  • A place where all the definitions in the class are stored
In particular, it has a namespace 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}
Now let's roll the dice:
>>> d1.roll()
>>> d1.dots
3
The instance variable 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
All instance we create have their own namespace for instance data
How does it work?
Here's the code for roll()
    def roll(self):
        self.dots = random.choice((1, 2, 3, 4, 5, 6)) 
In fact, the call d1.roll() is equivalent to (an abbreviation for) the call Dice.roll(d1)
  • Calling method roll() defined in class object Dice with instance d1 as the argument
Therefore, when 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 want
The 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 argument
    • def roll(self), etc.
  • Instance attributes referenced inside the class definition must have the self prefix
    • self.dots = random.choice((1, 2, 3, 4, 5, 6)), etc.
  • When calling a method attribute, self is passed implicitly
    • d1.roll(), not d1.roll(d1)

Example: The Quadratic Map

The quadratic map difference equation is given by


Let'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
Notice the call self.update()
  • Not self.update(self), because self is passed implicitly
  • Not just update()
    • Because we are referencing an attribute of the instance self that was passed in to generate_series() as a parameter
Here's an example of usage:
>>> 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
Here's an example of usage
>>> 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
Note: if we replace evaluate with __call__ then
>>> p.__call__(1) == p(1)
This is an example of a special method

Exercise

Recall the empirical distribution function


The 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
Example of usage
>>> 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: