Functions

Storing individual Python commands for re-use is one thing. Creating a function that can be repeatedly applied to different input data is quite another, and of huge importance in coding.


Teaching note

In some languages (Visual Basic and Fortran being examples): subroutines and functions. Subroutines perform actions, functions return results (given inputs). In Python there is no distinction: any function can both return results and perform actions.


There is a standard structure for a function in Python. Here we have

def name(arguments):
    """
    Comments
    """
    return value

The def keyword says that what follows is a function. Again, the name of the function follows the same rules and conventions as variables and files. The colon : at the end of the first line is essential: everything that follows that is indented will be the code to be executed when the function is called. The indentation is also essential. As soon as the indentation stops, the function stops.

Here is a simple example, that you can type directly into the console or into a file:


In [1]:
def add(x, y):
    """
    Add two numbers
    
    Parameters
    ----------
    
    x : float
        First input
    y : float
        Second input
    
    Returns
    -------
    
    x + y : float
    """
    return x + y

add(1, 2)


Out[1]:
3

We see that the line add(1, 2) is outside the function and so is executed. We can also call the function repeatedly:


In [2]:
print(add(3, 4))
print(add(10.61, 5.99))


7
16.6

The lengthy comment at the start of the function is very useful to remind yourself later what the function should do. You can see this information by typing


In [3]:
help(add)


Help on function add in module __main__:

add(x, y)
    Add two numbers
    
    Parameters
    ----------
    
    x : float
        First input
    y : float
        Second input
    
    Returns
    -------
    
    x + y : float

You can also view this in spyder by typing add in the Object window of the Help tab in the top right.

We can save the function to a file and re-use the function by importing the file. Create a new file in the spyder editor containing

def add(x, y):
    """
    Add two numbers

    Parameters
    ----------

    x : float
        First input
    y : float
        Second input

    Returns
    -------

    x + y : float
    """
    return x + y

and save it as script3.py. Then in the console check that it works as expected:


In [4]:
import script3
script3.add(1, 2)


Out[4]:
3
Exercise

Define your own function that divides two numbers. Save it in a script, script4.py. Check that you can import it, that it works correctly, and that help(script4.divide) gives useful information, and that looking at the help in Spyder gives the same useful information.

if statements and flow control

We often need to make a decision whether to do something, or to do something else. In nearly all languages this involves an if statement in some form, where a logical (Boolean) condition is used to "control the flow" of the program - that is, control which statements are executed when.

The logical conditions in Python evaluate to the special values True and False:


In [5]:
5 > 4


Out[5]:
True

In [6]:
4 == 5


Out[6]:
False

The Python notation is similar to that with functions:


In [7]:
count = 0

if count == 0:
    print("There are no items.")
elif count == 1:
    message("There is 1 item.")
else:
    print("There are" + count + " items.")


There are no items.

The keyword that Python uses is the lower case if. To add additional branches to the condition, Python uses the "Else If" which is contracted to elif. The condition (in this case!) compares a variable, count, to a number, using the equality comparison ==. Once again, as in the case of functions, the line containing the if definition is ended with a colon (:), and the commands to be executed are indented.

We can include as many branches of the if statement as we like using multiple elif statements. We do not need to use any elif statements, nor an else, unless we want (or need) to. We can nest if statements inside each other.

Exercise

Write a nested if statement that prints out if number is

  1. even or odd
  2. if even can it be divided by 4.

Note: modular division in Python is a % b.


In [8]:
number = 8

if number % 2 == 0:
    print(number, " is even.")
    if number % 4 == 0:
        print(number, " can be divided by 4.")
else:
    print(number, " is odd.")


8  is even.
8  can be divided by 4.

Loops

We will often want to run the same code many times on similar input. Let us suppose we want to add $n$ to $3$, where $n$ is every number between $1$ and $5$. We could do:


In [9]:
print(add(3, 1))
print(add(3, 2))
print(add(3, 3))
print(add(3, 4))
print(add(3, 5))


4
5
6
7
8

This is tedious and there's a high chance of errors.

In most programming languages you can define a loop that repeats commands. There are two possible types of loop: one that you know in advance how many times it will execute, and one that executes until something happens (at a point you may not be able to predict). We will focus on the former.

In Python the notation is a for loop:


In [10]:
for n in 1, 2, 3, 4, 5:
    print(add(3, n))
print("Loop has ended")


4
5
6
7
8
Loop has ended

The syntax has similarities to the syntax for functions. The line defining the loop starts with for, specifies the values that n takes, and ends with a colon. The code that is executed inside the loop is indented.

As a short-hand for integer loops, we can use the range function:


In [11]:
for n in range(1, 6):
    print("n =", n)
for m in range(3):
    print("m =", m)
for k in range(2, 7, 2):
    print("k =", k)


n = 1
n = 2
n = 3
n = 4
n = 5
m = 0
m = 1
m = 2
k = 2
k = 4
k = 6

We see that

  • if two numbers are given, range returns all integers from the first number up to but not including the second in steps of $1$;
  • if one number is given, range starts from $0$;
  • if three numbers are given, the third is the step.

In fact Python will iterate over any collection of objects: they do not have to be integers:


In [12]:
for thing in 1, 2.5, "hello", add:
    print("thing is ", thing)


thing is  1
thing is  2.5
thing is  hello
thing is  <function add at 0x103bf78c8>

This is very often used in Python code: if you have some way of collecting things together, Python will happily iterate over them all.

Exercise

Write a loop to check by brute force if an integer n is prime, by checking if any integer less than n divides it. Turn it into a function that returns True if n is prime and False otherwise. Test the function by printing every prime less than $50$. Find the first $k$ such that $k$ is prime and $2^k-1$ is not prime.


In [13]:
n = 17
prime = True
for divisor in range(2, n):
    if n % divisor == 0:
        prime = False
print(n, " is prime? ", prime)


17  is prime?  True

In [14]:
def isprime(n):
    """
    Check if a number is prime (brute force)
    
    Parameters
    ----------
    
    n : positive integer
        Number to check
        
    Returns
    -------
    
    True/False
    """
     
    prime = True
    for divisor in range(2, n):
        if n % divisor == 0:
            prime = False
    return prime

In [15]:
for n in range(2, 50):
    if isprime(n):
        print(n, " is prime.")


2  is prime.
3  is prime.
5  is prime.
7  is prime.
11  is prime.
13  is prime.
17  is prime.
19  is prime.
23  is prime.
29  is prime.
31  is prime.
37  is prime.
41  is prime.
43  is prime.
47  is prime.

In [16]:
k = 1
while not(isprime(k)) or isprime(2**k - 1):
    k = k + 1
print(k, "is the first k such that 2^k-1 is not prime.")


11 is the first k such that 2^k-1 is not prime.

Containers, sequences, lists, arrays

So what are the Python ways of collecting things together? In many languages there are arrays, which are often like vectors or matrices. They are ordered, can be indexed, but where the index starts from varies between languages.

In Python there are many ways of collecting objects together. The first two to look at are tuples and lists.

Tuples

A tuple is a sequence with fixed size, whose entries cannot be modified:


In [17]:
t1 = (0, 1, 2, 3, 4, 5)
print(t1[0])
print(t1[3])


0
3

We see that to access individual entries we use square brackets and the number of the entry, starting from $0$. All Python tuples and lists start from $0$. To check that it cannot be modified:


In [18]:
t1[0] = 1


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-18-9e1f4de27f17> in <module>()
----> 1 t1[0] = 1

TypeError: 'tuple' object does not support item assignment

We can use slicing to access many entries at once:


In [19]:
print(t1[1:4])


(1, 2, 3)

As with the range function, the notation <start>:<end> returns the entries from (and including) <start> up to, but not including, <end>.

We can use negative numbers to access from the right of the sequence: -1 is the last entry, -2 the next-to-last, and so on:


In [20]:
print(t1[-1])


5

Lists

A list is a sequence with a size that can change, and whose entries can be modified:


In [21]:
l1 = [0, 1, 2, 3, 4, 5]
print(l1[3])
l1[3] = 7
print(l1[3])
l1.append(6)
print(l1)


3
7
[0, 1, 2, 7, 4, 5, 6]

The same slicing notation can be used, and now can be used to assignment:


In [22]:
l1[0:2] = l1[4:6]
print(l1)


[4, 5, 2, 7, 4, 5, 6]

Crucially, lists and tuples can contain anything. As with loops, there is no restriction on types, and things can be nested:


In [23]:
l2 = [0, 1.2, "hello", ["a", 3, 4.5], (0, (1.1, 2.3, 4))]
print(l2[1])
print(l2[3][0])


1.2
a

Dictionaries

Both lists and tuples are ordered: there are accessed by an integer giving there location in the sequence. This doesn't always make sense. Consider an algorithm which depends on parameters $\omega, \Gamma, N$. We want to keep the parameters together, but there's no logical order to them. Instead we can use a dictionary, which is an unordered Python container:


In [24]:
d1 = {"omega": 1.0, "Gamma": 5.7, "N": 100}
print(d1["Gamma"])


5.7

As there is no order we access dictionaries using the key. To loop over a dictionary, we take advantage of Python's loose iteration rules:


In [25]:
for key in d1:
    print("Key is", key, "value is", d1[key])


Key is omega value is 1.0
Key is Gamma value is 5.7
Key is N value is 100

There is a shortcut to allow you to get both key and value in one go:


In [26]:
for key, value in d1.items():
    print("Key is", key, "value is", value)


Key is omega value is 1.0
Key is Gamma value is 5.7
Key is N value is 100

Numpy arrays

We've seen python's built-in lists for storing data. However for mathematical purposes (particular based on linear algebra) the list is not ideal. Instead the numpy library contains the more powerful array datatype. Arrays are essentially a more powerful form of lists which make it easier to handle data. Most importantly, they allow us to apply operations to all elements of an array at once, rather than looping over the elements one-by-one.

To see this, let's create a list and a numpy array, both containing the same data.


In [27]:
import numpy

In [28]:
# python list
l = [[1., 2., 3.],
     [4., 5., 6.],
     [7., 8., 9.]]

a = numpy.array([[1., 2., 3.],
                 [4., 5., 6.],
                 [7., 8., 9.]])

print('list l = {}'.format(l))
print('numpy array a = {}'.format(a))


list l = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]]
numpy array a = [[ 1.  2.  3.]
 [ 4.  5.  6.]
 [ 7.  8.  9.]]

Accessing elements of numpy arrays is very similar to accessing elements of lists, but with slightly less typing. To access elements from an n-dimensional list, we have to use multiple square brackets, e.g. l[0][4][7][8]. For a numpy array, we separate the indices using a comma: a[0, 4, 7, 8].


In [29]:
print(l[1][2])
print(a[1,2])


6.0
6.0

Let's say we now want to square every element of the array. For this 2d list, we would need a for loop:


In [30]:
import copy
squared = copy.deepcopy(l)
for i in range(3):
    for j in range(3):
        squared[i][j] = l[i][j]**2
print(squared)


[[1.0, 4.0, 9.0], [16.0, 25.0, 36.0], [49.0, 64.0, 81.0]]

Note that here we used the function deepcopy from the copy module the copy the list l. If we had simply used squared = l, when we the assigned the elements of squared new values, this would also have changed the values in l. This is in contrast to the simple variables we saw before, where changing the value of one will leave the values of others unchanged.

For numpy arrays, applying operations across the entire array is much simpler:


In [31]:
print(a**2)


[[  1.   4.   9.]
 [ 16.  25.  36.]
 [ 49.  64.  81.]]

Numpy has a range of array manipulation routines for rearranging and manipulating elements, such as those below.


In [32]:
# transpose
a.T


Out[32]:
array([[ 1.,  4.,  7.],
       [ 2.,  5.,  8.],
       [ 3.,  6.,  9.]])

In [33]:
# reshape
numpy.reshape(a, (1,9))


Out[33]:
array([[ 1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9.]])

In [34]:
# stack arrays horizontally
numpy.hstack((a,a,a))


Out[34]:
array([[ 1.,  2.,  3.,  1.,  2.,  3.,  1.,  2.,  3.],
       [ 4.,  5.,  6.,  4.,  5.,  6.,  4.,  5.,  6.],
       [ 7.,  8.,  9.,  7.,  8.,  9.,  7.,  8.,  9.]])

We can also apply logical operations to entire arrays:


In [35]:
a > 5


Out[35]:
array([[False, False, False],
       [False, False,  True],
       [ True,  True,  True]], dtype=bool)

If you've used Matlab before, you may be familiar with logical indexing. This is a way of accessing elements of a array that satisfy some criteria, e.g. all the elements which are greater than 0. We can also do this with numpy arrays using boolean array indexing:


In [36]:
a[a > 5]


Out[36]:
array([ 6.,  7.,  8.,  9.])
Exercise

The bubble sort algorithm sorts a list of numbers into ascending order. It loops over all the elements of the list from the start to the end. It compares the element to all "later" elements. If the later one is smaller, swap the elements.

Write a function that takes a list l and uses bubble sort to return a sorted list. Test it on l = [5, 1, 4, 2, 8].


In [37]:
def bubblesort(unsorted): 
    """
    Sorts an array using bubble sort algorithm

    Parameters
    ---------
    unsorted : list
        The unsorted list

    Returns
    -------
    sorted : list
        The sorted list (in place)
    """
    last = len(unsorted)
    # All Python lists start from 0
    for i in range(last):
        for j in range(i+1, last):
            if unsorted[i] > unsorted[j]:
                temp = unsorted[j]
                unsorted[j] = unsorted[i]
                unsorted[i] = temp
    return unsorted

In [38]:
l = [5, 1, 4, 2, 8]
print(bubblesort(l))


[1, 2, 4, 5, 8]