# Functions

A function takes input parameters, executes a series of computations with those inputs and then returns a final output value. Functions give us an efficient way to save and reuse a block of code over and over again with different input values. There are built-in functions in the standard Python library and we can define our own functions.

## Built-in Functions

The standard Python library has a collection of built-in functions ready for us to use. We have already seen a few of these functions in previous sections such as type(), print() and sum(). The following is a list of built-in functions that we'll use most often:

Function Description
print(object) print object to output
type(object) return the type of object
abs(x) return the absolute value of x (or modulus if x is complex)
int(x) return the integer constructed from float x by truncating decimal
len(sequence) return the length of the sequence
sum(sequence) return the sum of the entries of sequence
max(sequence) return the maximum value in sequence
min(sequence) return the minimum value in sequence
range(a,b,step) return the range object of integers from a to b (exclusive) by step
list(sequence) return a list constructed from sequence
sorted(sequence) return the sorted list from the items in sequence
reversed(sequence) return the reversed iterator object from the items in sequence
enumerate(sequence) return the enumerate object constructed from sequence
zip(a,b) return an iterator that aggregates items from sequences a and b

Use the function print() to display values:

pi = 3.14159
print(pi)

3.14159


Use the function type() to see the datatype of a value:

type(pi)

float


Use the function abs() to compute the absolute value of a real number:

x = -2019
abs(x)

2019


Or compute the magnitude of a complex number:

z = 3 - 4j
abs(z)

5.0


Use the function int() to truncate a float into an int:

pi = 3.14159
int(pi)

3


The function truncates floats always towards 0:

c = -1.2345
int(c)

-1


Use the function len() to compute the length of a sequence:

primes = [2,3,5,7,11,13,17,19,23,29,31,37,41]
len(primes)

13


Use the function sum() to compute the sum of a sequence:

one_to_hundred = range(1,101)
sum(one_to_hundred)

5050


Use the functions max() and min() to compute the maximum and minimum values in a sequence.

random = [8,27,3,7,6,14,28,19]
print(max(random))
print(min(random))

28
3


Use the function list() to convert a sequence (such as a range or a tuple) into a list:

list(range(0,10,2))

[0, 2, 4, 6, 8]


Use the function sorted() to sort a sequence:

sorted_random = sorted(random)
print(random)
print(sorted_random)

[8, 27, 3, 7, 6, 14, 28, 19]
[3, 6, 7, 8, 14, 19, 27, 28]


Use the function reversed() to reverse the order of a sequence:

reversed_random = list(reversed(random))
print(random)
print(reversed_random)

[8, 27, 3, 7, 6, 14, 28, 19]
[19, 28, 14, 6, 7, 3, 27, 8]


Use the function enumerate() to enumerate a sequence:

squares = [n**2 for n in range(0,6)]
print(squares)
enum_squares = list(enumerate(squares))
print(enum_squares)

[0, 1, 4, 9, 16, 25]
[(0, 0), (1, 1), (2, 4), (3, 9), (4, 16), (5, 25)]


Use the function zip() to combine sequences into a list of pairs:

random_1 = [-2,4,0,5]
random_2 = [7,-1,9,3]
random_zip = list(zip(random_1,random_2))
print(random_zip)

[(-2, 7), (4, -1), (0, 9), (5, 3)]


Notice in the last three examples reversed(), enumerate() and zip() we use the function list() to create a list from the output of each function. This is because these functions return iterator objects (similar to range objects) which only yield values when explicitly told to do so.

## Defining Functions

Let's begin with a simple example. Define a function which returns the average of a sequence of numbers:

def average(x):
"Compute the average of the values in the sequence x."
sum_x = sum(x)
length_x = len(x)
return sum_x / length_x


The main points to observe are:

1. Start the function definition with the def keyword.
2. Follow def with the name of the function.
3. Follow the function name with the list of input parameters separated by commas and within parentheses.
4. End the def statement with a colon :.
5. Indent the body of the function by 4 spaces.
6. Use the return keyword to specify the output of the function (but it is not always necessary).
7. The second line is a documentation string (enclosed in quotation marks " ... ") which describes the function.

In Python, code blocks are defined using indentation. This means that lines of code indented the same amount are considered one block. In the example above, the four indented lines below the def statement form the body of the function.

Notice that there is no output when we execute the cell containing the function definition. This is because we've only defined the function and it's waiting for us to use it! We need to call the function with values for the input parameters and then the function will compute and return the output value.

Let's test our function:

average([1,2,3,4])

2.5


The function returns the expected value. Success!

## Documentation Strings

The first line after the def statement in a function definition should be a documentation string (or docstring). A docstring is text (enclosed in double quotes " ... " or triple quotes ''' ... ''') which describes your function. Use triple quotes for a multiline docstring. See the Python documentation for all the conventions related to documentation strings.

A helpful feature of the Jupyter notebook is the question mark operator ?. This will display the docstring of a function. Keep this in mind when writing your docstrings: other people will read your docstring to learn how to use your function.

For example, use the question mark ? to view the documentation for the built-in function sum():

sum?

[0;31mSignature:[0m [0msum[0m[0;34m([0m[0miterable[0m[0;34m,[0m [0;34m/[0m[0;34m,[0m [0mstart[0m[0;34m=[0m[0;36m0[0m[0;34m)[0m[0;34m[0m[0;34m[0m[0m
[0;31mDocstring:[0m
Return the sum of a 'start' value (default: 0) plus an iterable of numbers

When the iterable is empty, return the start value.
This function is intended specifically for use with numeric values and may
reject non-numeric types.
[0;31mType:[0m      builtin_function_or_method


I recommend (but it's up to you) a style similar to NumPy's style guide for docstrings:

def function_name(param1,param2,param3):
'''First line is a one-line general summary.

A longer paragraph describing the function and
relevant equations or algorithms used in the
function.

Parameters
----------
param1 : datatype
Describe the parameter.
param2 : datatype
Describe the parameter.
param3 : datatype
Describe the parameters and continue with more
details if necessary on a new set of indented lines.

Returns
-------
datatype
A description of the output of the function and also
describe special behaviour.

Examples
--------
>>> function_name(1,2,3)
1.2345
'''



See these examples and these examples.

## Keyword Arguments

When we define functions, we list the input parameters. These are called positional parameters (or positional arguments) because the position in the def statement determines which parameter is which.

def poly(x,y):
"Compute x + y**2."
return x + y**2

poly(1,2)

5

poly(2,1)

3


A keyword argument allows us to insert default values for some parameters and we call them by name and the order doesn't matter.

For example, let's write a function called psum which takes numbers x and y and keyword argument p (with default value p=1) and returns the sum of powers

$$x^p + y^p$$

def psum(x,y,p=2):
return x**p + y**p

psum(1,2)

5

psum(1,2,p=1)

3

psum(2,3,p=3)

35


In this function, x and y are positional arguments and p is a keyword argument.

An exampleof a function with many keyword arguments is the function pandas.read_csv in the pandas package:

import pandas as pd

pd.read_csv?


So many keyword arguments! The keyword arguments I use most often are encoding, skiprows and usecols.

Comments in a Python program are plain text descriptions of Python code which explain code to the reader. Python will ignore lines which begin with the hash symbol # and so we use the hash symbol to write comments to explain the steps in a program. See the examples below.

## Lambda Functions

Lambda functions are small anonymous functions created with the lambda keyword:

function_name = lambda parameter: return_value


The expression on the right side of the assignment operator = is an anonymous lambda function and we assign it to the variable name function_name. Lambda functions are useful in at least two ways:

• Define a short function in a single line code
• Define a function within some other Python expression

For example, the function average defined above can be written in a single concise line:

average = lambda x: sum(x)/len(x)


The lambda keyword indicates that what follows is a function definition. The variable name x before the colon is the input parameter and the expression after the colon is the return value. Let's verify our lambda function returns the correct values:

average([1,2,3,4])

2.5

average([1,2,1,2])

1.5


Create lambda functions with several input parameters by listing variable names separated by commas. For example, let's create a function called hypotenuse which takes input parameters x and y and returns the length of the hypotenuse of the right angle triangle with sides x and y.

hypotenuse = lambda x,y: (x**2 + y**2)**0.5


Verify our functin returns the correct values:

hypotenuse(3,4)

5.0

hypotenuse(5,12)

13.0


## Examples

### Riemann Zeta Function

The Riemann zeta function is the infinite sum

$$\zeta(s) = \sum_{n = 1}^{\infty} \frac{1}{n^s}$$

Write a function called zeta which takes 2 input parameters s and N and returns the partial sum:

$$\sum_{n=1}^N \frac{1}{n^s}$$

def zeta(s,N):
"Compute the Nth partial sum of the zeta function at s."
terms = [1/n**s for n in range(1,N+1)]
partial_sum = sum(terms)
return partial_sum


Let's test our function on input values for which we know the result:

zeta(1,1)

1.0

zeta(2,2)

1.25


Now let's use our function to approximate special values of the Riemann zeta function:

$$\zeta(2) = \frac{\pi^2}{6} \hspace{10mm} \text{and} \hspace{10mm} \zeta(4) = \frac{\pi^4}{90}$$

Compute the partial sum for $s=2$ and $N=100000$:

zeta(2,100000)

1.6449240668982423


Compare to an approximation of the special value $\pi^2/6$:

3.14159265**2/6

1.6449340630890041


Compute the partial sum for $s=4$ and $N=100000$:

zeta(4,100000)

1.082323233710861


Compare to an approximation of the special value $\pi^4/90$:

3.14159265**4/90

1.0823232287641997


### Harmonic Mean

Write a function called harmonic_mean which takes an input parameter s, a list of numbers $x_1, \dots, x_n$ of length $n$, and returns the harmonic mean of the sequence:

$$\frac{n}{\frac{1}{x_1} + \frac{1}{x_2} + \cdots + \frac{1}{x_n}}$$

def harmonic_mean(s):
"Compute the harmonic mean of the numbers in the sequence s."
n = len(s)
terms = [1/s[i] for i in range(0,n)]
result = n/sum(terms)
return result


Let's test our function:

harmonic_mean([1,1,1,1])

1.0

harmonic_mean([1,2,3])

1.6363636363636365


### Riemann Sums

Write a function called mn_integral which takes input parameters m, n, a, b and N and returns the (right) Riemann sum: $$\int_a^b f(x) \, dx \approx \sum_{k=1}^N f(x_k) \Delta x \ \ , \ \ f(x) = \frac{x^m + 1}{x^n + 1}$$

and $\Delta x = (b-a)/N$ and $x_k = a + k \Delta x$.

def mn_integral(m,n,a,b,N):
'''Compute the right Riemann sum for the function f(x) = (x^m + 1)/(x^n + 1)
on interval [a,b] with a partition of N subintervals of equal size.
'''
delta_x = (b - a)/N
x = [a + k*delta_x for k in range(0,N+1)]
terms = [(x[k]**m + 1)/(x[k]**n + 1)*delta_x for k in range(1,N+1)]
riemann_sum = sum(terms)
return riemann_sum


Let's test our function on input for which we know the result. Let $m=0$, $n=1$, $a=0$, $b=1$ and $N=2$. Then $x_0 = 0$, $x_1 = 1/2$, $x_2 = 1$ and $\Delta x = 1/2$, and we compute:

\begin{aligned} \sum_{k=1}^N f(x_k) \Delta x &= \sum_{k=1}^2 \frac{x_k^0 + 1}{x_k^1 + 1} \Delta x \\ &= \frac{2}{(1/2) + 1} \cdot \frac{1}{2} + \frac{2}{1 + 1} \cdot \frac{1}{2} \\ &= \frac{7}{6} \end{aligned}

mn_integral(0,1,0,1,2)

1.1666666666666665

7/6

1.1666666666666667


Let's test our function on another example. Let $m=1$, $n=2$, $a=0$, and $b=1$. We can solve this integral exactly:

\begin{aligned} \int_0^1 \frac{x + 1}{x^2 + 1} dx &= \int_0^1 \frac{x}{x^2 + 1} dx + \int_0^1 \frac{1}{x^2 + 1} dx \\ &= \left. \left( \frac{1}{2} \ln(x^2 + 1) + \arctan x \right) \right|_0^1 \\ &= \frac{1}{2} \ln(2) + \frac{\pi}{4} \end{aligned}

Approximate this integral with a Riemann sum for $N=100000$:

mn_integral(1,2,0,1,100000)

1.1319717536649336


Since $\pi \approx 3.14159265$ and $\ln(2) \approx 0.69314718$, we compare to the approximation:

0.5*0.69314718 + 3.14159265/4

1.1319717525000002


Our function computes the expected values!

## Exercises

Exercise 1. Write a function called p_norm which takes input parameters sequence and p where sequence is a list of numbers $x_1, \dots, x_n$ and p is a nonzero number. The function returns the $p$-norm:

$$\left( \frac{1}{n} \sum_{i=1}^n | x_i |^p \right)^{1/p}$$

Plug in large positive values of $p$ and various lists of numbers to verify

$$\lim_{p \to \infty} \left( \frac{1}{n} \sum_{i=1}^n | x_i |^p \right)^{1/p} = \max { |x_1| , \dots, |x_n| }$$

Plug in large negative values of $p$ and various lists of numbers to verify

$$\lim_{p \to -\infty} \left( \frac{1}{n} \sum_{i=1}^n |x_i|^p \right)^{1/p} = \min { |x_1| , \dots, |x_n| }$$

Exercise 2. Write a function called arctan_taylor which takes input parameters x and N and return the Taylor polynomial of degree $N$ of the function $\arctan x$ evaluated at x:

$$\sum_{k=0}^N (-1)^k \frac{x^{2k + 1}}{2k + 1}$$

Exercise 3. Write a function called zips which takes input parameters a and b, where a and b are lists of the equal length, and returns the list of tuples which aggregates the sequence. (In other words, write your own version of the built-in function zip()... without using zip() of course.) For example zips([-1,3,4,0],[5,7,1,-9]) returns the list [(-1, 5), (3, 7), (4, 1), (0, -9)].

Exercise 4. Write a function called sqrt_integral which takes input parameters u, p and N and returns the Riemann sum (using the midpoints $x_k^*$ of a partition of size $N$):

$$\int_0^u \frac{1}{\sqrt{1 + x^p}} dx \approx \sum_{k=1}^N \frac{1}{\sqrt{1 + (x_k^*)^p}} \Delta x$$

where $\Delta x = u/N$ and $x_k^* = (x_k + x_{k-1})/2$ for endpoints $x_k = k \Delta x$.