Chapter 21 User defined functions, modules, and libraries

21.1 User defined functions

We have seen in lectures the basic syntax for defining and calling a function. Here we will focus on some useful variants of this, and discuss some of the issues that can arise.

When defining a function we should ideally include a docstring at the beginning. To save space when presenting code I have generally not done so in my very short examples. Docstrings can be regarded as a special kind of comment function. They are used at the start of a module or a function (or a class, although we are not going to consider such objects here) and should provide a basic description of what that object is for. This can then be accessed using the help(obj) command, which returns the docstring corresponding to the object obj.

We have seen that it is good practice to include comments in your code, and the same is true of docstrings.

Functions can have multiple input parameters, and we have seen that some of these can be made optional by providing default values for them. We have been calling our functions using what is known as the , which is the natural way in which one would think to use a function.

Given a function such as \[f(x,y)=x^2+y^3\] we would expect the value of \(f(2,4)\) to be \(2^2+4^3\), as the positions of the values 2 and 4 correspond to the positions of \(x\) and \(y\) in our function.

Python also allows us to call a function using the . If our function above was defined in Python code then we could call it using f(x=2, y=4). Here we are using the fact that each variable in our function corresponds to a different keyword (here just x or y) and so if we say what value each keyword corresponds to then Python can calculate the value of the function. One advantage of this approach is that we do not need to remember what order the variables are listed in the definition of f, as f(x=2, y=4) gives the same value as f(y=4, x=2).

It is even possible to use both positional and keyword methods at the same time!

For an example of how this all works, consider the very simple function shown below.

def optionals(x, y=0, z=0):
    return x ** 2 + 2 * y ** 2 + 3 * z ** 2

print(optionals(4), optionals(4,1), optionals(4,1,3))
print()
print(optionals(4, y=1), optionals(4, z=1), optionals(z=4, x=3))

This implements in Python the function \[f(x,y,z)=x^2+2y^2+3z^2\] with default values \(y=0\) and \(z=0\). The main body of the code then calculates various different examples.

The first print line outputs the values of \(f(4,0,0)\), \(f(4,1,0)\), and \(f(4,1,3)\). These are 16, 18, and 45 respectively. Here we are using the positional method to determine which values correspond to \(x\), \(y\), and \(z\).

The final print line is more complicated. The first output is \(f(4,1,0)\), where we are identifying 4 with \(x\) by its position, and \(y\) with 1 by a keyword. The second output is \(f(4,0,1)\) which is similar, and the final output is purely using keywords to give \(f(3,0,4)\). These equal 18, 19, and 57 respectively.

Notice that the final two outputs correspond to the case when we give the function values for \(x\) and \(z\), but not for \(y\). It is impossible to do this using the positional method, as the only way we can give \(z\) a value using position if it is the third variable entered (which would mean that we had given a value for \(y\) as well).

As we have seen, the keyword method has a number of advantages. However, it does make the code we enter substantially longer — particularly when (as we should!) we use descriptive variable names instead of just single letters. Which method you use is entirely up to you, but it is well worth remembering that both are possible.

We saw in lectures that a function can return several values. For convenience the example is reproduced below.

def test(x,y):
    quotient = x // y
    remainder = x % y
    return quotient, remainder

a, b = test(10, 7)
print("Seven divides ten", a, "times with remainder", b)

Here we want to focus on the initially mysterious looking line just below the end of the test function. This assigns a value to a and a value to b at the same time!

The reason this works is that we lied a little when we described the tuple syntax. We said that a tuple is something of the form (x, y, z) when in fact the brackets are only there to make the code more readable. So a tuple can be written down just as a sequence of objects separated by commas.

This is why a tuple with one element has to be written in the form (x,).

So if we write something like x, y = 4, 6 this is just the same as if we write (x, y) = (4, 6) and so what we are really doing when we return our function is define one two element tuple in terms of another.

For this reason many introductions to Python will mention at an early stage that you can use this syntax to define multiple variables at the same time. It saves a lot of space, and if it does not confuse you then that is fine.

In fact it turns out that multiple assignments such as this can be done more generally. If you have any ordered object then we can use the same method to assign the elements of it to variables. So

x, y, z = ["red", "green", "blue"]

corresponds to setting x = "red", y = "green", and z = "blue". Similarly (but perhaps more surprisingly)

x, y, z = "cat"

corresponds to setting x = "c", y = "a", and z = "t".

This may seem like a bit of a trick, but it sometimes turns out to be a very useful way to quickly assign certain values corresponding to the different parts of a complex object.

21.2 Mutable and immutable variables

In our discussion of local and global variables in lectures, we saw that globally defined lists (and sets) behaviour in an unexpected manner if they are modified inside a function, and that this is not true of other variables such as numbers and strings. We will now try to explain why this is the case. This is rather confusing, so you may wish to skip the rest of this section.

The reason for this is to do with how Python regards and stores such variables. Variables have an identity which never changes (you can think of this like the variable’s location in the computer’s memory). They also have a value. We say that an object is mutable if its value can change, otherwise we call an object immutable.

Lists and sets are mutable in Python, whereas strings, ranges, tuples, Booleans, and the various types of numbers are immutable.

When we try to update the value of an immutable variable we are actually destroying the old version and creating a new version (with a new identity). So if we say x = x + 5 we are actually taking the value of the original x variable stored in some location, adding 5 to this value, and storing the result in a new variable stored in a different location which we are now calling x instead.

On the other hand, when we change the value of a mutable variable we are not changing it’s identity (the location where it is stored) but only the value that is stored there.

We can now explain some of the strange behaviours of lists. If we have a list a and we define a second list with the assignment b = a then we are giving two different names to the same object; ie to the same location in memory. But now if we change the value of a we are changing the value but not changing the identity of a, ie we are merely updating the value stored in the memory location labelled by a. So the variable b which is just the same location in memory has also been updated to the new value. This explains why the code on slide 7 in Lecture 15 behaved as it did.

Things can get even more confusing if an immutable object contains a mutable one (for example if a tuple has a list as one of its elememnts). Fortunately we can largely ignore these subtleties and just remember to be careful when copying lists or using globally defined lists inside a function.

21.3 Modules

We saw in lectures how to import a set of functions that we have previously stored in a file. However, we did not discuss where this file should be stored.

When working with Python you will be storing your files in some directory on your computer (unless you are using Gogle Colab and working in the cloud). To import a file of functions that you have already written make sure that it is in the same directory as your main program file. It is possible to import from other directories, but this is surprisingly tricky, so we will not cover it here.

So far when we have written our functions we have included them in the same file as some other code which typically we used to test those functions or to do some other task. If we want to use the functions elsewhere we could copy them into a new file or delete the rest of the code.

However, it is more convenient to slightly modify the file so that the main body of code can still be run if we want to, but is ignored when we import the file into something else. To do this we just have to add the (rather mysterious looking!) line

if __name__ == "__main__":

after the functions we have written but before the main body of code. (As this is the start of an if block the remaining code below it will all need to be indented.) This will ensure that the main body of code is only run if the current file is run, and not if it is imported into a different file. Note that we use a pair of underscores on each side of name and main.

Here is an example of this in use.

def optionals(x, y=0, z=0):
    return x ** 2 + 2 * y ** 2 + 3 * z ** 2

if __name__ == "__main__":

    print(optionals(4), optionals(4,1), optionals(4,1,3))
    print()
    print(optionals(4, y=1), optionals(4, z=1), optionals(z=4, x=3))

This is a modified version of an example from earlier, which will now not print the various values at the end if the file is imported elsewhere. As this example shows, this means we can test our functions as we go and not worry about these tests appearing when we import them to our main program.

21.4 Libraries

Python comes with a Standard Library which includes a number of useful modules. These are always available and do not need to be separately installed (but they do need to be imported into your code). Some of the most useful ones (for our purposes) are

  • math which provides some standard maths functions
  • cmath which provides some standard maths functions for complex numbers
  • statistics which can be used to calculate some basic statistical properties of data
  • random which provides tools for generating random selections
  • datetime which provides tools for working with dates and times

but there are many others.

There are many more libraries that are not part of the default Python installation. One reason we have recommended using Anaconda for this module is that it comes with many of the most popular libraries packaged with it, so that they do not need to be separately installed. The same is true of Google Colab.

If you want to check whether a given library is available, create a simple file with the single line import libraryname where libraryname is the library which you wish to import. If you run this file and get an error message then you will need to install the library yourself.

There are various ways to install libraries depending on whether you are using Anaconda or Colab (or something else) and sometimes depending on which operating system you are using. We should not need to install any additional libraries in this module, so we will not go into details here; googling will quickly given you the method that works for your set-up if you later need to do this.