Friday, December 23, 2011

Computational Economics lecture 10


Functions

Functions are pieces of code which perform specific tasks.
We have already met some functions:
  • The built-in functions, made available when you start Python
    • max()len()range(), etc.
  • Functions in modules, which have to be imported
    • math.sqrt()random.uniform(), etc.
It is also useful to be able to define our own functions
  • Improve the clarity of your code
  • Avoid writing the same thing twice
    • This is always a bad idea (time consuming, harder to maintain your code)
For example, let's say you write a program to send an email to your mother on her birthday
  • Program checks the date each day
  • If date == mother_birthday then sends an email
    • Happy birthday Mum
Your mother is happy, because you never forget her birthday
Now you decide you want to send an email on your father's birthday, sister's birthday, etc.
If you write a new program each time you will repeat a lot of the same code
Better to write one function email(name) which sends one email
  • Message: Happy birthday name
  • To: name@gmail.com
The argument name can be any string
The program looks something like this (in pseudocode)
birthdays = {'mum': '02-07',
             'dad': '12-03',
             'sis': '07-11'}

define function email(name):
    message = 'Happy birthday %s!!' % name
    address = '%s@gmail.com' % name
    send message to address  # This pseudocode is pretty easy to code up in Python

repeat every morning:
    get date
    for name, birthday in birthdays.items():
        if birthday == date:
            email(name)

Enhancing Clarity with Functions

Let's look at some examples how functions improve clarity

Example 1

Recall that we previously studied the following problem
  • Compute an approximation to pi using Monte Carlo
    • Note that if U is uniform on the unit square, then the probability U is in a subset B is equal to the area of B
    • And that if U_1,...,U_n are IID copies of U, then the fraction in B converges to the probability of B as n gets large
    • Finally, recall that for a circle, area = pi * radius^2
The solution we used was
## Filename: circle.py
## Author: John Stachurski

# Since pi = area / radius**2, we need to estimate the area of the circle
# and then divide by radius**2 = (1/2)**2 = 1/4.  First we estimate the area
# by sampling bivariate uniforms and looking at the fraction that fall into
# the circle.

from random import uniform
from math import sqrt

n = 100000

count = 0
for i in range(n):
    U, V = uniform(0, 1), uniform(0, 1)
    d = sqrt((U - 0.5)**2 + (V - 0.5)**2)
    if d < 0.5:
        count += 1

area_estimate = count / float(n)

print area_estimate * 4  # dividing by radius**2
At first glance it's not so clear what this program does
Let's try to make it more readable using a function
## Filename: circle2.py
## Author: John Stachurski

from random import uniform
from math import sqrt

# First we define a new function, called in_circle

def in_circle(x, y):
    """
    Tests whether (x,y) is in the circle of radius 0.5, centered on 
    the point (0.5, 0.5).
    """
    if sqrt((x - 0.5)**2 + (y - 0.5)**2) < 0.5:
        return True
    else:
        return False

# Now the main loop

n = 100000
count = 0
for i in range(n):
    U, V = uniform(0, 1), uniform(0, 1)
    if in_circle(U, V):
        count += 1

area_estimate = count / float(n)
print area_estimate * 4  # dividing by r**2
Okay, so the code is longer, but the logic is clearer (which is more important)
We define a function in_circle()
  • Takes as arguments a two numbers x, y
  • Returns True if the pair is in the cirle and False otherwise
In the main loop we call the function
  • If it returns True, increment count
The syntax for defining functions is explained below

Example 2

Previously we considered the following problem
  • Write a program which prints one random outcome of following game
    • 10 flips of an unbiased coin
    • If 3 consecutive heads occur, pays one dollar
    • If not, pays nothing
Next we wrote a program to estimate average payoff
## Filename: 3heads3.py
## Author: John Stachurski

from random import uniform

n = 100000

outcomes = []
for i in range(n):
    payoff = 0
    count = 0
    for j in range(10):
        U = uniform(0, 1)
        count = count + 1 if U < 0.5 else 0
        if count == 3:
            payoff = 1
            break
    outcomes.append(payoff)

print sum(outcomes) / float(n)
The main loop is long and complicated
We can clarify the logic by generating payoffs as a function:
## Filename: 3heads4.py
## Author: John Stachurski

from random import uniform

def sample():
    """
    Generates random payoff from one round of the game.
    If 3 consecutive heads occur, returns 1. Else, returns 0.
    """
    payoff = 0
    count = 0
    for j in range(10):
        U = uniform(0, 1)
        count = count + 1 if U < 0.5 else 0
        if count == 3:
            payoff = 1
            break
    return payoff

# Main loop

n = 100000
outcomes = [sample() for i in range(n)]  # The loop
print sum(outcomes) / float(n)
Again, the program is a bit longer, but the logic is clearer
  • Program is broken down into conceptual parts
Notice that the function sample() has no arguments
  • We call it using sample(), not sample

Defining and Calling Functions

The syntax used to define a function is
def <name>(<parameters>):  # `def` is a Python keyword used to declare functions
    <body>
Here <parameters> is a list of zero or more names/identifiers
Let's look at an example that replicates the count() method for strings
def f(string, letter):
    count = 0
    for s in string:
        if s == letter:
            count += 1
    return count
Calling f(string, letter) is the same as string.count(letter)
Suppose this is written in a script and we run it
  • The function is written in memory and stored as an object
  • The identifier f is bound to this object
Here we run it in IDLE



The function object is created and f is bound to it
>>> f
<function f at 0x86e5f0c>
The function object has been stored at memory location 0x86e5f0c
>>> type(f)
<type 'function'>
Now we can call (i.e., use) f
>>> f('godzilla', 'g')  # Calling f()
1
>>> y = f('foo', 'o')
>>> y
2

Flow

Here's our function definition again
def f(string, letter):
    count = 0
    for s in string:
        if s == letter:
            count += 1
    return count
Let's step through execution of the statement
>>> y = f('foo', 'o') 
  • The interpreter creates a "stack frame" to hold variable names
    • The identifier string is registered in stack frame, bound to 'foo'
    • The identifier letter is registered in stack frame, bound to 'o'
    • Other "local" variables (counts) are stored in the stack frame too
    • Execution of the function continues until return count
    • The identifier y is bound to the value of count
  • The stack frame and its contents are discarded
    • Values of stringlettercount and s are lost
Typically, execution of the function body continues until it hits return
def f():
    print 'This line is printed'
    return 1
    print 'But this line is not'
It is possible to have no return statement
def f():
    print 'foo bar'
In this case
  • execution stops at the end of the code block
  • the function returns a special value called None
We can have multiple return statements
  • Execution terminates when the first one is met
def f(x):
    if x < 0:
        return 'negative'
    else:
        return 'nonnegative' 

Arguments to functions

Any objects can be passed to a function as arguments
We have seen functions passed numbers and strings
Lists and tuples are okay too
Here's a function which is passed a list/tuple X and returns sum(X)
def sum2(X):
    count = 0
    for x in X:
        count += x
    return count
In fact we can pass functions as arguments just as easily!
def function_one(function_two):
    for i in range(5):
        print function_two(i)

def g(n):
    return 'foo' * n
Here is what happens
john@godzilla:~$ python -i test.py 
>>> function_one(g)

foo
foofoo
foofoofoo
foofoofoofoo
Can you see how this works?

Return Values

Functions always return one object
Returning two or more objects is not possible
For example, this doesn't return two values (why?)
def f():
    return 1
    return 2
Here is another attempt to return two objects (integers):
def f():
    return 1, 2
But this returns one object: a tuple

Doc Strings

The syntax for defining functions was given above as
def <name>(<parameters>):
    <body>
Actually there is another optional element: a doc string
def <name>(<parameters>):
    "<doc string>"
    <body>
Previously we saw the folloing example
def in_circle(x, y):
    """
    Tests whether (x,y) is in the circle of radius 
    0.5, centered on (0.5, 0.5).
    """
    if sqrt((x - 0.5)**2 + (y - 0.5)**2) < 0.5:
        return True
    else:
        return False
Single quotes, double quotes, triple quotes all fine
The doc string is just a comment, so we could also use a hash
def in_circle(x, y):
    # Tests whether (x,y) is in the circle of radius 
    # 0.5, centered on (0.5, 0.5).
    if sqrt((x - 0.5)**2 + (y - 0.5)**2) < 0.5:
        return True
    else:
        return False
The advantage of a doc string is that it is available at runtime
>>> in_circle
<function in_circle at 0x866048c>
>>> help(in_circle)
Help on function in_circle in module __main__:

in_circle(x, y)
    Tests whether (x,y) is in the circle of radius 
    0.5, centered on (0.5, 0.5).

Exercises

Problem 1:
Write a function which takes a string as an argument and returns the number of capital letters in the string
Problem 2:
Write a function which takes two sequences A and B as arguments and returns True if every element in A is also an element of B, else False
Problem 3:
Write a function which takes as arguments
  • A function f which maps a float into a float
  • An interval [a, b] (i.e., two floats a and b)
  • An integer n
  • A number x
Returns
  • the piecewise linear interpolation value at x,
  • based on n evenly spaced grid points a = x[0] < x[1] < ... < x[n-1] = b
Here is an example with n = 3



Solutions

Solution to Problem 1:
def f(string):
    count = 0
    for letter in string:
        if letter == letter.upper():
            count += 1
    return count
Solution to Problem 2:
def f(A, B):
    subset = True
    for a in A:
        if a not in B:
            subset = False
    return subset
Solution to Problem 3:
## Filename: linapprox.py
## Author: John Stachurski

from __future__ import division

def linapprox(f, a, b, n, x):
    """
    Evaluates piecewise linear interpolant of f at x on the interval [a, b],
    with n evenly spaced grid points.
    Parameters: 
        f is a function
        x, a and b are numbers with a <= x <= b
        n is an integer.
    Returns: 
        A number (the interpolant evaluated at x)
    """
    length_of_interval = b - a
    num_subintervals = n - 1
    step = length_of_interval / num_subintervals  # Distance between grid points

    # Find the first grid point that is larger than x
    point = a
    while point <= x:
        point += step
    # Now x must lie between the gridpoints (point - step) and point
    u, v = point - step, point  

    return f(u) + (x - u) * (f(v) - f(u)) / (v - u)

0 comments: