Course: Deep Learning for Solving and Estimating Dynamic Models in Economics and Finance
Script reference: Front matter and Appendix~E — function definitions, scope, *args/**kwargs
Notebook role: primer (pre-course self-study; skip if you write Python every day)
Author: Simon Scheidegger
Python Basics 7: Functions¶
The function objects in Python allow you to give a name to a series of expressions that may or may not return a result.
To syntax for defining a function in Python is as follows:
def function_name(some_inputs):
some expressions
return something (optional)Functions with no inputs that return nothing¶
Let’s start first with functions that have no inputs and return nothing.
def print_hello():
"""
Prints hello.
"""
print('Hello there!')First, let’s run this:
print_hello()Hello there!
See for yourself that the type of print_hello is a function:
type(print_hello)functionThe other thing that I want you to notice is the text in the triple quotes below the function definition.
This is called the docstring of the function.
You use it to document what the function does so that other people know how to use it.
The docstring is what the help() function sees.
Check this out:
help(print_hello)Help on function print_hello in module __main__:
print_hello()
Prints hello.
Finally, let’s see if print_hello() returns anything. Let’s try to grab whatever it returns and print it.
res = print_hello()
print('Res is: ', res)Hello there!
Res is: None
Alright! Now you see why None is useful. A function that returns nothing, returns None.
Questions¶
Modify the function so that it prints hellow there 5 times. Make sure you also modify the docstring to reflect the new behavior.
Real functions¶
When I am talking about numerical functions, I mean things like or and so on. These functions typically take a single input that is a real number and they return also a single input which is a real number. Here are some examples:
def square(x):
"""
Calculates the square of ``x``.
Arguments:
x - The real number you wish to square
Returns: The square of ``x``.
"""
return x ** 2help(square)Help on function square in module __main__:
square(x)
Calculates the square of ``x``.
Arguments:
x - The real number you wish to square
Returns: The square of ``x``.
square(2)4square(23.0)529.0Because real functions are used so often, there is actually a shortcut. It is called lambda functions.
To define a lambda function, the syntax is:
func_name = lambda inputs: single_expression_you_want_to_returnHere is the square function in a single line:
alt_square = lambda x: x ** 2alt_square(2)4alt_square(23.0)529.0You will see me using both.
Let’s finish this section by evaluating the square function on all numbers from 0 to 100 and putting the result in a list. This is a very commonly occuring process required for plotting functions.
# The xs on which you want to evaluate square(x)
xs = range(0, 101)
# The corresponding ys (empty list to be grown gradually)
ys = []
# Loop over all the x's
for x in xs:
# Evaluate the function at x:
y = square(x)
# Add the value to the list
ys.append(y)
print(ys)[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401, 2500, 2601, 2704, 2809, 2916, 3025, 3136, 3249, 3364, 3481, 3600, 3721, 3844, 3969, 4096, 4225, 4356, 4489, 4624, 4761, 4900, 5041, 5184, 5329, 5476, 5625, 5776, 5929, 6084, 6241, 6400, 6561, 6724, 6889, 7056, 7225, 7396, 7569, 7744, 7921, 8100, 8281, 8464, 8649, 8836, 9025, 9216, 9409, 9604, 9801, 10000]
Now, there is a simpler way to do this using what is known as a list generator. This is a rather advanced Python construct, but because I cannot resist using it, I am going to show you here how it works. Basically, it is mirroring the mathematical definitions of a set. Here it is:
ys = [square(x) for x in xs]
print(ys)[0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100, 121, 144, 169, 196, 225, 256, 289, 324, 361, 400, 441, 484, 529, 576, 625, 676, 729, 784, 841, 900, 961, 1024, 1089, 1156, 1225, 1296, 1369, 1444, 1521, 1600, 1681, 1764, 1849, 1936, 2025, 2116, 2209, 2304, 2401, 2500, 2601, 2704, 2809, 2916, 3025, 3136, 3249, 3364, 3481, 3600, 3721, 3844, 3969, 4096, 4225, 4356, 4489, 4624, 4761, 4900, 5041, 5184, 5329, 5476, 5625, 5776, 5929, 6084, 6241, 6400, 6561, 6724, 6889, 7056, 7225, 7396, 7569, 7744, 7921, 8100, 8281, 8464, 8649, 8836, 9025, 9216, 9409, 9604, 9801, 10000]
Questions¶
Write a function that calculates the mean of a list of a number. Try it out on the
xs.
# Your code hereFunctions with many arguments¶
You can have as many arguments as you want in a function.
Here is a function that calculates the p norm of a vector.
The p norm of the vector is defined to be:
Here it is:
def norm(x, p):
"""
Calculates the ``p``-norm of the vector ``x``.
Arguments:
x - A list of numbers.
p - A positive number.
"""
res = 0.0
for x_i in x:
res += x_i ** p
return res ** (1 / p)Let’s try this:
x = [1, 2, 3]
norm(x, 2)3.7416573867739413norm(x, 3)3.3019272488946263norm(x, 100)3.0Now, let’s try to run the same thing without specifying the p input:
norm(x)---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/tmp/ipykernel_22265/2267588480.py in <module>
----> 1 norm(x)
TypeError: norm() missing 1 required positional argument: 'p'We get the error that the p argument of the function is missing.
However, p=2 is the most common choice because it corresponds to the standard Euclidean norm.
We can rewrite the function so that by default p=2. Such arguments are called default arguments.
Here is how:
def norm(x, p=2):
"""
Calculates the ``p``-norm of the vector ``x``.
Arguments:
x - A list of numbers.
p - A positive number (default p=2)
"""
res = 0.0
for x_i in x:
res += x_i ** p
return res ** (1 / p)Now you can call norm(x):
norm(x)3.7416573867739413If you want to use another p, you can do:
norm(x, 3.0)3.3019272488946263or (more intuitively):
norm(x, p=3.0)3.3019272488946263Alirght, this is nice! Let’s now try to break our function.
We can break it in various ways.
First, we can break it by passing a nonpositive p.
Here you go:
norm(x, p=0)---------------------------------------------------------------------------
ZeroDivisionError Traceback (most recent call last)
/tmp/ipykernel_22265/3750166826.py in <module>
----> 1 norm(x, p=0)
/tmp/ipykernel_22265/2132562949.py in norm(x, p)
10 for x_i in x:
11 res += x_i ** p
---> 12 return res ** (1 / p)
ZeroDivisionError: division by zeroor this one:
norm(x, p=-1)0.5454545454545455In the first case, we get an error message. In the second case we do not get any error message.
However, the assumption that p is positive is clearly violated.
To force the function to give us an error message when its assumptions are violated, we can use assert statements.
Here is how:
def norm(x, p=2):
"""
Calculates the ``p``-norm of the vector ``x``.
Arguments:
x - A list of numbers.
p - A positive number (default p=2)
"""
# Check that the function assiumptions are satisfied
# Turn p into a float even if it is not one
p = float(p)
# Ensures taht p is positive
assert p > 0, 'p must be positive (p = {0:1.2f})'.format(p)
# The code that calculates the p-norm
res = 0.0
for x_i in x:
res += x_i ** p
return res ** (1 / p)Here is how this works. If you give it the right p, then it just works:
norm(x, p=2)3.7416573867739413Here is what happens with a bad p:
norm(x, p=-1)---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
/tmp/ipykernel_22265/911542282.py in <module>
----> 1 norm(x, p=-1)
/tmp/ipykernel_22265/3955484933.py in norm(x, p)
11 p = float(p)
12 # Ensures taht p is positive
---> 13 assert p > 0, 'p must be positive (p = {0:1.2f})'.format(p)
14 # The code that calculates the p-norm
15 res = 0.0
AssertionError: p must be positive (p = -1.00)Of course, we can still break the function in various ways:
norm(['a', 'b'], p=2)---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
/tmp/ipykernel_22265/777857029.py in <module>
----> 1 norm(['a', 'b'], p=2)
/tmp/ipykernel_22265/3955484933.py in norm(x, p)
15 res = 0.0
16 for x_i in x:
---> 17 res += x_i ** p
18 return res ** (1 / p)
TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'float'The norm of a list of characters does not make sense.
To add a check on whether or not the input x is a valid vector, we would have to wait until we introduce numerical arrays.
Questions¶
The infinity norm is defined to be:
Modify the code of the
norm(x,p)function so that whenp = math.inf, it uses gives back the right result.
import math # for math.inf
# Your code here# Try this out:
norm(x,p=math.inf)1.0Organizing your code in functions¶
Remember the previous hands-on activity in which we wrote code to calculate the sum of an infinite series to a given tolerance.
If we wanted to change the series we had to modify the code by hand.
I will now show you how you can organize the code that we wrote in a function that accepts as an input any series you want.
The thing that you need to mediate a bit on is that the function calculate_series that we will create has an input a function a which can give you the result of the sequence you are summing for any , i.e., a(n) calculates .
Study this code:
def calculate_series(a, max_iter=100000, epsilon=1e-5, verbose=True):
"""
Calculate a given series to a desired tolerance.
Arguments
a - A function that specifies the sequence you want to sum.
max_iter - The maximum number of iterations, a positive integer
(default max_iter=10000).
epsilon - The tolerance, a positive float (default epsilon=1e-5).
verbose - A boolean specifying whether or not you want to print
something about the progress of the function
(default verbose=True).
Returns:
An estimate of the sum of a(n) for n = 0 to infinty.
"""
# Check assumptions
# There is a way to check if a is a function, but it is a bit advanced
# max_iter must be a positive integer
assert isinstance(max_iter, int), 'max_inter must be an integer'
assert max_iter > 0, 'max_iter must be positive'
# epsilon must be a positive float
assert isinstance(epsilon, float), 'epsilon must be a float'
assert epsilon > 0, 'epsilon must be positive'
# The result
res = 0.0
# Start a counter
n = 0
# Start the loop
while n <= max_iter:
# Compute the new term
a_n = a(n)
# Check if the absolute value of the new term is smaller than the tolerance
if abs(a_n) < epsilon:
# If it is indeed smaller, exit the loop
# you do this with the command
if verbose:
print('*** Converged in {0:d} iterations! ***'.format(n+1))
break
# Otherwise we just add the new term to our running sum
res += a_n
# Print something about the current iteration
if verbose and n % 10000 == 0:
print('Current iteration n = {0:10d}, sum so far: {1:1.12f}'.format(n, res))
# and increase the counter
n += 1
if verbose and n == max_iter + 1:
print('*** Stopped when maximum number of iterations ({0:d}) were reached! ***'.format(max_iter))
return resHere is the help of the function we just wrote:
help(calculate_series)Help on function calculate_series in module __main__:
calculate_series(a, max_iter=100000, epsilon=1e-05, verbose=True)
Calculate a given series to a desired tolerance.
Arguments
a - A function that specifies the sequence you want to sum.
max_iter - The maximum number of iterations, a positive integer
(default max_iter=10000).
epsilon - The tolerance, a positive float (default epsilon=1e-5).
verbose - A boolean specifying whether or not you want to print
something about the progress of the function
(default verbose=True).
Returns:
An estimate of the sum of a(n) for n = 0 to infinty.
Here is how to use it:
# First define a sequence
def a(n):
return (-1) ** n / (2 * n + 1)# Now run this:
calculate_series(a)Current iteration n = 0, sum so far: 1.000000000000
Current iteration n = 10000, sum so far: 0.785423160898
Current iteration n = 20000, sum so far: 0.785410662772
Current iteration n = 30000, sum so far: 0.785406496453
Current iteration n = 40000, sum so far: 0.785404413241
*** Converged in 50001 iterations! ***
0.7853931633974454And here is an even faster way to do the same thing using lambda functions:
calculate_series(lambda n: (-1) ** n / (2 * n + 1))Current iteration n = 0, sum so far: 1.000000000000
Current iteration n = 10000, sum so far: 0.785423160898
Current iteration n = 20000, sum so far: 0.785410662772
Current iteration n = 30000, sum so far: 0.785406496453
Current iteration n = 40000, sum so far: 0.785404413241
*** Converged in 50001 iterations! ***
0.7853931633974454