One construct that’s extremely useful and provided by almost all programming languages is functions.
We have already met several functions, such as
sqrt()function from NumPy and
In this lecture we’ll treat functions systematically and begin to learn just how useful and important they are.
One of the things we will learn to do is build our own user-defined functions
We will use the following imports.
%matplotlib inline import numpy as np import matplotlib.pyplot as plt plt.rcParams['figure.figsize'] = (10,6)
4.2. Function Basics¶
A function is a named section of a program that implements a specific task.
Many functions exist already and we can use them off the shelf.
First we review these functions and then discuss how we can build our own.
4.2.1. Built-In Functions¶
Python has a number of built-in functions that are available without
We have already met some
Two more useful built-in functions are
bools = False, True, True all(bools) # True if all are True and False otherwise
any(bools) # False if all are False and True otherwise
The full list of Python built-ins is here.
4.2.2. Third Party Functions¶
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(2020)
4.3. Defining Functions¶
In many instances, it is useful to be able to define our own functions.
This will become clearer as you see more examples.
Let’s start by discussing how it’s done.
4.3.1. Basic Syntax¶
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:
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.
defis a Python keyword used to start function definitions.
def new_abs_function(x):indicates that the function is called
new_abs_functionand that it has a single argument
The indented code is a code block called the function body.
returnkeyword indicates that
abs_valueis 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:
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'
Functions without a return statement automatically return the special Python object
4.3.2. Keyword Arguments¶
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
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-', label="white noise")is different from
plot('b-', x, label="white noise")
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
They can be modified as follows
f(2, a=4, b=5)
4.3.3. The Flexibility of Python Functions¶
As we discussed in the previous lecture, Python functions are very flexible.
Any number of functions can be defined in a given file.
Functions can be (and often are) defined inside other functions.
Any object can be passed to a function as an argument, including other functions.
A function can return any kind of object, including functions.
We will give examples of how straightforward it is to pass a function to a function in the following sections.
4.3.4. One-Line Functions:
lambda keyword is used to create simple functions on one line.
For example, the definitions
def f(x): return x**3
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
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.
4.3.5. Why Write Functions?¶
User-defined functions are important for improving the clarity of your code by
separating different strands of logic
facilitating code reuse
(Writing the same thing twice is almost always a bad idea)
We will say more about this later.
4.4.1. Random Draws¶
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()
We will break this program into two parts:
A user-defined function that generates a list of random variables.
The main part of the program that
calls this function to get data
plots the data
This is accomplished in the next program
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.
4.4.2. Adding Conditions¶
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.
We are passing the argument
Uas a string, which is why we write it as
Notice that equality is tested with the
For example, the statement
a = 10assigns the name
ato the value
a == 10evaluates to either
False, depending on the value of
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
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
While these lines are executed, the names
np.random.uniformare “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)
Here we created another name for the built-in function
max(), which could
then be used in identical ways.
In the context of our program, the ability to bind new names to functions means that there is no problem passing a function as an argument to another function—as we did above.
4.5. Recursive Function Calls (Advanced)¶
This is not something that you will use every day, but it is still useful — you should learn it at some stage.
Basically, a recursive function is a function that calls itself.
For example, consider the problem of computing \(x_t\) for some t when
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
a frame is where the local variables of a given function call are held
stack is memory used to process function calls
a First In Last Out (FILO) queue
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(n)returns \(n!\) for any positive integer \(n\).
In addition, try to add a new argument for your function. The argument takes a function
fthat transforms n to \(f(n) = n^2 + 1\) if n is even, and \(f(n) = n^2\) if n is odd. The default value should be \(f(n) = n\).
The default case
factorial(3)should return \(3!\)
factorial(3,f)should return \(9!\)
factorial(2,f)should return \(5!\)
Try to use lambda expressions to define the function
Solution to Exercise 4.1
Here’s one solution for part 1
def factorial(n): k = 1 for i in range(n): k = k * (i + 1) return k factorial(4)
Adding the lambda expression
def factorial(n,f = lambda x: x): k = 1 for i in range(f(n)): k = k * (i + 1) return k factorial(9) # default
f = lambda x: x**2 + 1 if x % 2 == 0 else x**2 factorial(3, f) # odd (equivalent to factorial(9))
factorial(2, f) # even (equivalent to factorial(5))
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\).
Solution to Exercise 4.2
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
Flip an unbiased coin 10 times.
If a head occurs
kor more times consecutively within this sequence at least once, pay one dollar.
If not, pay nothing.
Second, write another function that does the same task except that the second rule of the above random device becomes
If a head occurs
kor more times within this sequence, pay one dollar.
Use no import besides
from numpy.random import uniform.
Solution to Exercise 4.3
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)
0 0 0 1 2 0 1 0 0 0
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)
0 1 1 1 2 3 3 4 5 5
4.7. Advanced Exercises¶
In the following exercises, we will write recursive functions together.
We will use more advanced syntaxes such as list comprehensions to test our solutions against a list of inputs.
If you are not familiar with these concepts, feel free to come back later.
The Fibonacci numbers are defined by
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\).
Solution to Exercise 4.4
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)])
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
For this exercise, rewrite the function
factorial(n) in exercise 1 using recursion.
Solution to Exercise 4.5
Here’s the standard solution
def recursion_factorial(n): if n == 1: return n else: return n * recursion_factorial(n-1)
Here’s a simplified solution
def recursion_factorial_simplified(n): return n * recursion_factorial(n-1) if n != 1 else n
Let’s test them
print([recursion_factorial(i) for i in range(1, 10)])
[1, 2, 6, 24, 120, 720, 5040, 40320, 362880]
print([recursion_factorial_simplified(i) for i in range(1, 10)])
[1, 2, 6, 24, 120, 720, 5040, 40320, 362880]