Functions are an extremely useful construct provided by almost all programming.
We have already met several functions, such as
sqrt()
function from NumPy andprint()
functionIn this lecture we’ll
We will use the following imports.
import numpy as np
import matplotlib.pyplot as plt
A function is a named section of a program that implements a specific task.
Many functions exist already and we can use them as is.
First we review these functions and then discuss how we can build our own.
Python has a number of built-in functions that are available without import
.
We have already met some
max(19, 20)
print('foobar')
str(22)
type(22)
The full list of Python built-ins is here.
If the built-in functions don’t cover what we need, we either need to import functions or create our own.
Examples of importing and using functions were given in the previous lecture
Here’s another one, which tests whether a given year is a leap year:
import calendar
calendar.isleap(2024)
In many instances it’s useful to be able to define our own functions.
Let’s start by discussing how it’s done.
Here’s a very simple Python function, that implements the mathematical function $ f(x) = 2 x + 1 $
def f(x):
return 2 * x + 1
Now that we’ve defined this function, let’s call it and check whether it does what we expect:
f(1)
f(10)
Here’s a longer function, that computes the absolute value of a given number.
(Such a function already exists as a built-in, but let’s write our own for the exercise.)
def new_abs_function(x):
if x < 0:
abs_value = -x
else:
abs_value = x
return abs_value
Let’s review the syntax here.
def
is a Python keyword used to start function definitions.def new_abs_function(x):
indicates that the function is called new_abs_function
and that it has a single argument x
.return
keyword indicates that abs_value
is the object that should be returned to the calling code.This whole function definition is read by the Python interpreter and stored in memory.
Let’s call it to check that it works:
print(new_abs_function(3))
print(new_abs_function(-3))
Note that a function can have arbitrarily many return
statements (including zero).
Execution of the function terminates when the first return is hit, allowing code like the following example
def f(x):
if x < 0:
return 'negative'
return 'nonnegative'
In a previous lecture, you came across the statement
plt.plot(x, 'b-', label="white noise")
In this call to Matplotlib’s plot
function, notice that the last argument is passed in name=argument
syntax.
This is called a keyword argument, with label
being the keyword.
Non-keyword arguments are called positional arguments, since their meaning is determined by order
plot(x, 'b-')
differs from plot('b-', x)
Keyword arguments are particularly useful when a function has a lot of arguments, in which case it’s hard to remember the right order.
You can adopt keyword arguments in user-defined functions with no difficulty.
The next example illustrates the syntax
def f(x, a=1, b=1):
return a + b * x
The keyword argument values we supplied in the definition of f
become the default values
f(2)
They can be modified as follows
f(2, a=4, b=5)
As we discussed in the previous lecture, Python functions are very flexible.
In particular
We will give examples of how straightforward it is to pass a function to a function in the following sections.
lambda
¶
The lambda
keyword is used to create simple functions on one line.
For example, the definitions
def f(x):
return x**3
and
f = lambda x: x**3
are entirely equivalent.
To see why lambda
is useful, suppose that we want to calculate $ \int_0^2 x^3 dx $ (and have forgotten our high-school calculus).
The SciPy library has a function called quad
that will do this calculation for us.
The syntax of the quad
function is quad(f, a, b)
where f
is a function and a
and b
are numbers.
To create the function $ f(x) = x^3 $ we can use lambda
as follows
from scipy.integrate import quad
quad(lambda x: x**3, 0, 2)
Here the function created by lambda
is said to be anonymous because it was never given a name.
User-defined functions are important for improving the clarity of your code by
(Writing the same thing twice is almost always a bad idea)
We will say more about this later.
Consider again this code from the previous lecture
ts_length = 100
ϵ_values = [] # empty list
for i in range(ts_length):
e = np.random.randn()
ϵ_values.append(e)
plt.plot(ϵ_values)
plt.show()
def generate_data(n):
ϵ_values = []
for i in range(n):
e = np.random.randn()
ϵ_values.append(e)
return ϵ_values
data = generate_data(100)
plt.plot(data)
plt.show()
When the interpreter gets to the expression generate_data(100)
, it executes the function body with n
set equal to 100.
The net result is that the name data
is bound to the list ϵ_values
returned by the function.
Our function generate_data()
is rather limited.
Let’s make it slightly more useful by giving it the ability to return either standard normals or uniform random variables on $ (0, 1) $ as required.
This is achieved in the next piece of code.
def generate_data(n, generator_type):
ϵ_values = []
for i in range(n):
if generator_type == 'U':
e = np.random.uniform(0, 1)
else:
e = np.random.randn()
ϵ_values.append(e)
return ϵ_values
data = generate_data(100, 'U')
plt.plot(data)
plt.show()
Hopefully, the syntax of the if/else clause is self-explanatory, with indentation again delimiting the extent of the code blocks.
Notes
U
as a string, which is why we write it as 'U'
.==
syntax, not =
.a = 10
assigns the name a
to the value 10
.a == 10
evaluates to either True
or False
, depending on the value of a
.Now, there are several ways that we can simplify the code above.
For example, we can get rid of the conditionals all together by just passing the desired generator type as a function.
To understand this, consider the following version.
def generate_data(n, generator_type):
ϵ_values = []
for i in range(n):
e = generator_type()
ϵ_values.append(e)
return ϵ_values
data = generate_data(100, np.random.uniform)
plt.plot(data)
plt.show()
Now, when we call the function generate_data()
, we pass np.random.uniform
as the second argument.
This object is a function.
When the function call generate_data(100, np.random.uniform)
is executed, Python runs the function code block with n
equal to 100 and the name generator_type
“bound” to the function np.random.uniform
.
generator_type
and np.random.uniform
are “synonyms”, and can be used in identical ways.This principle works more generally—for example, consider the following piece of code
max(7, 2, 4) # max() is a built-in Python function
m = max
m(7, 2, 4)
This is an advanced topic that you should feel free to skip.
At the same time, it’s a neat idea that you should learn it at some stage of your programming career.
Basically, a recursive function is a function that calls itself.
For example, consider the problem of computing $ x_t $ for some t when
$$ x_{t+1} = 2 x_t, \quad x_0 = 1 \tag{4.1} $$
Obviously the answer is $ 2^t $.
We can compute this easily enough with a loop
def x_loop(t):
x = 1
for i in range(t):
x = 2 * x
return x
We can also use a recursive solution, as follows
def x(t):
if t == 0:
return 1
else:
return 2 * x(t-1)
What happens here is that each successive call uses it’s own frame in the stack
This example is somewhat contrived, since the first (iterative) solution would usually be preferred to the recursive solution.
We’ll meet less contrived applications of recursion later on.
Recall that $ n! $ is read as “$ n $ factorial” and defined as $ n! = n \times (n - 1) \times \cdots \times 2 \times 1 $.
We will only consider $ n $ as a positive integer here.
There are functions to compute this in various modules, but let’s write our own version as an exercise.
In particular, write a function factorial
such that factorial(n)
returns $ n! $
for any positive integer $ n $.
Here’s one solution:
def factorial(n):
k = 1
for i in range(n):
k = k * (i + 1)
return k
factorial(4)
The binomial random variable $ Y \sim Bin(n, p) $ represents the number of successes in $ n $ binary trials, where each trial succeeds with probability $ p $.
Without any import besides from numpy.random import uniform
, write a function
binomial_rv
such that binomial_rv(n, p)
generates one draw of $ Y $.
If $ U $ is uniform on $ (0, 1) $ and $ p \in (0,1) $, then the expression U < p
evaluates to True
with probability $ p $.
Here is one solution:
from numpy.random import uniform
def binomial_rv(n, p):
count = 0
for i in range(n):
U = uniform()
if U < p:
count = count + 1 # Or count += 1
return count
binomial_rv(10, 0.5)
First, write a function that returns one realization of the following random device
k
or more times consecutively within this sequence at least once, pay one dollar.Second, write another function that does the same task except that the second rule of the above random device becomes
k
or more times within this sequence, pay one dollar.Use no import besides from numpy.random import uniform
.
Here’s a function for the first random device.
from numpy.random import uniform
def draw(k): # pays if k consecutive successes in a sequence
payoff = 0
count = 0
for i in range(10):
U = uniform()
count = count + 1 if U < 0.5 else 0
print(count) # print counts for clarity
if count == k:
payoff = 1
return payoff
draw(3)
Here’s another function for the second random device.
def draw_new(k): # pays if k successes in a sequence
payoff = 0
count = 0
for i in range(10):
U = uniform()
count = count + ( 1 if U < 0.5 else 0 )
print(count)
if count == k:
payoff = 1
return payoff
draw_new(3)
In the following exercises, we will write recursive functions together.
The Fibonacci numbers are defined by
$$ x_{t+1} = x_t + x_{t-1}, \quad x_0 = 0, \; x_1 = 1 \tag{4.2} $$
The first few numbers in the sequence are $ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 $.
Write a function to recursively compute the $ t $-th Fibonacci number for any $ t $.
Here’s the standard solution
def x(t):
if t == 0:
return 0
if t == 1:
return 1
else:
return x(t-1) + x(t-2)
Let’s test it
print([x(i) for i in range(10)])
Rewrite the function factorial()
in from Exercise 1 using recursion.
Here’s the standard solution
def recursion_factorial(n):
if n == 1:
return n
else:
return n * recursion_factorial(n-1)
Let’s test it
print([recursion_factorial(i) for i in range(1, 10)])