Unit 4: Basic Control Structures and Functions¶
Overview and Learning Objectives
This unit briefly introduces basic Python control structures (including if
, for
, and while
), which are similar to most other programming languages. Then, we look at how to create a function in Python using the keyword def
. In particular, we discuss the difference between default and nondefault arguments. Also, we give some guidelines on how to document a function using a suitable docstring. Finally, we show how one can measure the runtime for executing a function or small code snippets using the Python module timeit
. In the first three exercises of this unit, you will implement some simple functions (which can be done in many ways) to familiarize yourself with the Python syntax. In Exercise 4, we consider the mathematical problem of whether a natural number is a prime number. After implementing a simple Python function that solves this task, you should look for more efficient algorithms for primality testing. Going beyond the task specification, you may implement different algorithms and compare their runtime behavior and execution time for increasing numbers. Finally, in Exercise 5, you find an accurate description of an algorithm for finding a root of a continuous function (i.e., an argument where the function assumes the value zero). While being a good exercise for implementing a mathematical procedure in executable Python code, this exercise also reviews some mathematical terms (e.g., interval, root, prime) that we think you should be familiar with. Furthermore, we want to make you aware that the notion of a function may appear in different contexts, e.g., once referring to a Python function and once to a mathematical function $f$ (which itself may serve as an argument to a Python function).
Basic Control Structures¶
In Python, there are basic control structures similar to most other programming languages. The most important control structures are:

if
: conditionally execute statements 
for
: repeat statements a specific number of times 
while
: repeat statements an indefinite number of times 
break
: terminate execution of awhile
orfor
loop 
continue
: continue execution of awhile
orfor
loop at top
For control structures such as if
, for
or while
, one has to use indentations (as part of the syntax). A typical Python convention is to use four spaces (or a tab indent). In the following, we give examples for each of these control structures. Let us start with an if
statement, which is written as follows:
n = 2
if n < 0:
print('Negative')
elif n > 0:
print('Positive')
else:
print('Zero')
The next example shows how to use a for
loop. Note that an iterable may be specified by a range, a list, a tuple, or even other structures.
for i in range(3):
print(i, end='')
print()
for i in ['a', 2, 'c', 'def']:
print(i, end='')
print()
for i in 'example':
print(i, end='')
print()
A while
loop is written as follows:
a = 0
while a < 5:
print(a, end='')
a += 1
The break
statement is used to terminate the loop containing it. This is demonstrated by the following example, which consists of two nested loops each containing a break
statement.
for k in 'abcd':
if k == 'c':
break
for i in '12345':
if i == '4':
break
print(k + i, end='')
The continue
statement is used to terminate only the current step of the loop. The loop then continues with the next iteration.
for k in 'abcd':
if k == 'c':
continue
for i in '12345':
if i == '4':
continue
print(k + i, end='')
Python Functions¶
One defines functions with the def
keyword. As variable names, function names may contain letters (a
, b
, ..., Y
, Z
) and the underscore (_
). All but the first character can also be positive integer number. Usually one uses lower case letters and underscores to separate words. The following function is named add
. It has three arguments a
, b
, and c
(with b
and c
having a default value). The return
keyword is succeeded by the return value.
def add(a, b=0, c=0):
"""Function to add three numbers
Notebook: PCP_04_control.ipynb
Args:
a: first number
b: second number (Default value = 0)
c: third number (Default value = 0)
Returns:
Sum of a, b and c
"""
print('Addition: ', a, ' + ', b, ' + ', c)
return a + b + c
print(add(5))
print(add(5, 2, 1))
print(add(5, c=4))
There are some rules and differences on how to use arguments without and with default values.
 All nondefault arguments must be specified in the function call.
 The order of default arguments may not be changed in the call.
 The default arguments are optional in a call. If a value is provided, the default value is overwritten.
 A function may have any number of default arguments. However, a default argument may not be followed by a nondefault argument.
These rules are illustrated by the previous examples. The next example demonstrates that a function may also have multiple return values (which are returned as a tuple):
def add_and_diff(a, b=0):
"""Function to add and subtract two numbers
Notebook: PCP_04_control.ipynb
Args:
a: first number
b: second number (Default value = 0)
Returns:
first: a + b
second: a  b
"""
return a + b, a  b
x = add_and_diff(3, 5)
print('result:', x, type(x))
x, y = add_and_diff(3, 5)
print('result:', x, y, type(x), type(y))
x, y = add_and_diff(3.0, 5)
print('result:', x, y, type(x), type(y))
It is important to document a function. In particular, one should always describe the purpose of a function as well as its input and output. This is exactly what a docstring is used for. As described in the Python Docstring Conventions, a docstring is a string literal that occurs as the first statement in the function. For consistency, it is advised to use """triple double quotes"""
around docstrings. Using the help()
function, the content of the docstring is shown. This is a useful feature when building your own libraries with many functions. In the following code cell, we show the docstring of the function add
defined above.
help(add)
Efficiency and Runtime¶
In the following example, we consider the task of computing the sum $\sum_{i=1}^n i$ for a given integer $n\in\mathbb{N}$. We first implement a naive function sum_n
that uses a simple for
loop. We then execute the function sum_n
in a while
loop for increasing $n=1,2,3,...$ and output the result until the sum exceeds $100$.
def sum_n(n):
"""Function that sums up the integers from 1 to n
Notebook: PCP_04_control.ipynb
Args:
n: Integer number
Returns:
s: Sum of integers from 1 to n
"""
s = 0
for n in range(1, n+1):
s = s + n
return s
n = 1
while sum_n(n) <= 100:
print(f'n= {n}, s={sum_n(n)}')
n += 1
Rather than using a for
loop, one may think more efficient functions to compute this sum. In the following code cell, we consider the function sum_n_numpy
that is based on the efficient NumPyfunction np.sum
. Furthermore, we consider the function sum_n_math
which uses a closed mathematical formula for calculating the sum. We then measure and compare the execution time for the three different implementations. To this end, we use the Python module timeit
, which provides a simple way to measure the execution time of small bits of Python code. As an alternative, one may also use the function time.time()
of the module time
.
import numpy as np
def sum_n_numpy(n):
"""Function that sums up the integers from 1 to n using numpy
Notebook: PCP_04_control.ipynb
Args:
n: Integer number
Returns:
s: Sum of integers from 1 to n
"""
s = np.sum(np.arange(1, n+1))
return s
def sum_n_math(n):
"""Function that sums up the integers from 1 to n using the idea by Gauss
Notebook: PCP_04_control.ipynb
Args:
n: Integer number
Returns:
s: Sum of integers from 1 to n
"""
s = n * (n + 1) // 2
return s
n = 1000
print(f'Computation with sum_n: n={n}, s={sum_n(n)}')
print(f'Computation with sum_n_numpy: n={n}, s={sum_n_numpy(n)}')
print(f'Computation with sum_n_math: n={n}, s={sum_n_math(n)}')
# Measuring runtime of different implementations
import timeit
executions = 100
n = 10000
runtime_av = timeit.timeit(lambda: sum_n(n), number=executions) / executions
print(f'Runtime for sum_n: {runtime_av * 1000:.6f} ms')
runtime_av = timeit.timeit(lambda: sum_n_numpy(n), number=executions) / executions
print(f'Runtime for sum_n_numpy: {runtime_av * 1000:.6f} ms')
runtime_av = timeit.timeit(lambda: sum_n_math(n), number=executions) / executions
print(f'Runtime for sum_n_math: {runtime_av * 1000:.6f} ms')
lambda args: expression
, where the expression is executed and the result is returned. For more details we refer to the Python Documentation.
Exercises and Results¶
import libpcp.control
show_result = True
give_me_a_number
Write a function
give_me_a_number
that has a string variable s
as input argument. When s
is the string 'large'
, the function should return the value $2^{100}$. If s
is 'small'
, it should return $2^{100}$. If s
is 'random'
, it should return a random number in the range $[0,1)$ (using np.random.rand
). In all other cases, the function should return np.nan
. As default, set the input argument to the string 'nan'
.
#<solution>
# Your Solution
#</solution>
libpcp.control.exercise_give_number(show_result=show_result)
#<solution>
# Your Solution
#</solution>
libpcp.control.exercise_row_mean(show_result=show_result)
Given a vector
x
(in form of a onedimensional NumPy array), write a function vector_odd_index
that outputs a vector y
consisting only of the elements of x
at an odd index position. If the input NumPy array x
is not a vector, the function vector_odd_index
should output y=None
. Note: Recall that Python indexing starts with index
0
(which is an even index).
#<solution>
# Your Solution
#</solution>
libpcp.control.exercise_odd(show_result=show_result)
An integer $n\in\mathbb{N}$ is called a prime number if $n>1$ and if $n$ is divisible only by $1$ and itself. Write a function
isprime
that inputs an integer n
and outputs the boolean value True
if n
is a prime number and False
otherwise.
 Test your function for $n=1$, $n=17$, $n=1221$, and $n=1223$.
 Use this function to output the first $20$ prime numbers.
 Discuss efficiency issues. Is there a more efficient way to compute the first $20$ prime numbers?
 Have a look at the algorithm Sieve of Eratosthenes, which is an ancient algorithm for finding all prime numbers up to any given limit.
#<solution>
# Your Solution
#</solution>
libpcp.control.exercise_isprime(show_result=show_result)
Let $f:\mathbb{R}\to\mathbb{R}$ be a continuous function and let $a,b\in\mathbb{R}$ be two numbers such that $aroot in the interval $[a,b]$. In other words, there is a number $r\in[a,b]$ such that $f(r)=0$. To find such a root, one can proceed as follows:
 Let $c = (a+b)/2$ be the center. If $f(a)=0$, $f(b)=0$, or $f(c)=0$, then we have found a root and we can stop.
 Otherwise, either $f(a)$ and $f(c)$, or $f(c)$ and $f(b)$ have opposite signs. In the first case, continue with the interval $[a,c]$, otherwise with the interval $[c,b]$.
 Iterate this intervalhalving procedure until the interval size is below a given threshold parameter (e.g., $10^{5}$) and a sufficient approximation for the root is obtained.
Write a function search_root
that implements this procedure (also known as bisection method). The input arguments should be f
, a
, b
, and thresh
. Here, f
is a Python function realizing a continuous function $f:\mathbb{R}\to\mathbb{R}$. The parameter thresh
is the threshold parameter, which should be set to the value $10^{5}$ by default. The function search_root
should check if a < b
and if the opposite sign condition for f(a)
and f(b)
is fulfilled (if not it should return the value np.nan
).
 Evaluate
search_root
forf
given by $f(x) = x^22$,a=0
, andb=2
.
(Note: You may define a separate Python functionf
that implements $f(x) = x^22$. This function can then be used as one of the input argument to the Python functionsearch_root
.)  Test
search_root
for the same function when usinga=2
andb=4
.  Test
search_root
for the same function when usinga=4
andb=2
.  Evaluate
search_root
forf
beingnp.sin
usinga=3
andb=4
.
search_root
) can be a function itself (e.g., f
). In Python, a function is simply an object that can be referred to in the same way one refers to a number, a string, or a list. For further explanations, we refer to the article on Passing a function as an argument to another function in Python.
#<solution>
# Your Solution
#</solution>
libpcp.control.exercise_root(show_result=show_result)