Functions#

We have already used functions.

For example, we have used the round function:

a = 3.1415
# Call the "round" function
b = round(a, 2)
b
3.14

We often need to define our own functions. Before we do, we need to go into more detail about what functions are for, and what they are.

Functions are like named recipes#

A function is a named recipe. It is a name we give to a set of steps to follow, a piece of code to run.

Thanks to the Berkeley team for this metaphor.

A recipe is the procedure to go from ingredients to a meal.

A function is the procedure to go from the arguments to the return value.

For example, I might have a recipe with the procedure to go from the ingredients: two eggs; butter; and cheese - to the meal - a cheese omelette.

The function round has the procedure to go from the arguments - two numbers, to the return value, which is the value of the first argument rounded to the number of digits specified in the second.

I could call my recipe “two egg cheese omelette”, or “recipe number 4”. Whatever I called it, it would be the same recipe. I might prefer a name that describes what the recipe makes, to help me remember.

Likewise, the name round refers to a procedure above. I could give it another name, like my_function, but round is a good name, because it helps me remember what the procedure does.

I say round has a procedure, but we can’t see what that procedure is, it’s buried inside the internal workings of Python.

Now we are going to write our own function, where we can see the procedure.

Revision on variables#

Here is an assignment statement:

a = 2

As we know, we can read this as “The variable ‘a’ gets the value 2”.

We also know that we have, on the left, a variable name, ‘a’, and on the right, an expression, that gives a value.

In this case, the expression on the right is 2. Python evaluates this expression, to make its own internal computer representation of the integer 2. Call this: Computer Representation (CR) of int 2.

After Python executes this statement, the name “a” points to the CR of int 2.

To continue the revision:

b = a * 4

The right side a * 4 is an expression. Python evaluates the expression. First it gets the value of a. This is the CR of int 2. Next it gets the value of 4. This is the CR of int 4. Then it multiplies these results to get an CR of int 8.

“b” now points to the CR of int 8.

Finally:

a = 3

“a” no longer points the CR of int 2, it points to the CR of int 3.

What value does “b” have now?

The same value as it had before. It pointed to the CR of int 8 before. Changing a has no effect on b.

Defining a function#

We define our function called double. It accepts one argument (ingredient), call that x. It’s procedure is to multiply the argument by 2. The return value is the argument multiplied by 2.

Here it is:

def double(x):
    d = x * 2
    return d

Let’s look at the first line:

def double(x):

The first word def tells Python we are defining a function.

The next word double is the name we will give to our function.

Between the parentheses, we have the function signature. This specifies how many arguments the function has. In our case, there is only one argument, named x.

Finally there is a colon : signifying the end of the signature.

As in for loops, the colon signifies that the next bit of code must be indented.

Here is the indented part:

    d = x * 2
    return d

This is the body of the function. It gives the function procedure; it defines what the function will do to its arguments, and what result it should return.

For example, here we call the function we just created:

double(4)
8

Notice that double(4) is a call expression.

So, what just happened?

  1. Python finds what double points to. It points to internal representation of our function (procedure).

  2. Next it sees the parenthesis ( and sees that we want to call our function.

  3. Now Python knows we want to call the function, it knows that there are one or more expressions inside the parentheses. In our case there is one, 4. As usual, it evaluates this expression to the CR of int 4.

  4. Now Python does the call. To do this it:

    1. Puts itself into function world (more on this later).

    2. Sets the new variable x to have the value CR of int 4, from above.

    3. Executes the code in the function body (procedure).

    4. The first line d = x * 2 is an assignment statement. x evaluates to CR of int 4, 2 evaluates to CR of int 2, so d has the value CR of int 8. This is how the statement would work anywhere in Python, function body or not.

    5. The next line starts with return. This is a return statement. When Python sees a return statement, it evaluates the expression to the right, to get the return value, then

    6. Pulls itself out of function world.

    7. Gives the return value as the final result of the call expression. This is CR of int 8.

We can run the function with any values for the argument.

double(2)
4

This time round, everything happened in the same way as before, except Python found the argument inside the parentheses evaluated to CR of int 2. Thus, in function world, x gets the value CR of 2, and the return value becomes CR of int 4.

Function world#

I cryptically used the term function world for the state that Python goes into when it calls a function.

This state has two important features.

Variables defined in functions have local scope#

The first feature of function world is that all variables defined inside function world, get thrown away when we leave function world.

We can see this if we run the following code in a notebook cell. This code runs in our usual top-level world, and so, not inside a function.

d
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[10], line 1
----> 1 d

NameError: name 'd' is not defined

Notice that, in the function, we set d to point to the result of x * 2. We called the function a couple of times, so we executed this statement a couple of times. But the d in the function, gets thrown away, when we come back from function world.

In technical terms, this is called scope. The scope of a variable, is the pieces of code in which the variable is visible. d can only be seen inside the function. Its scope is the function. We can also say that its scope is piece of code where it is defined, that is, it has local scope.

The same is true for x, the argument variable:

x
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[11], line 1
----> 1 x

NameError: name 'x' is not defined

The function has limited access to variables outside the function#

We have not seen this yet, but function world has limited access to variables defined at the top level.

We won’t go into much detail here, but the summary is that functions can see the values of variables defined at the top level, but they can’t change what top level variables point to. For example, say you have a variable a at the top level. A function can see and use the value of a, but it cannot change top-level a to point to a different value. We will come back to this later.

Python checks the function signature#

The signature for double is (a). That tells Python to expect one and only one argument. If we try to call it with no arguments (nothing inside the parentheses), we get an error:

double()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[12], line 1
----> 1 double()

TypeError: double() missing 1 required positional argument: 'x'

If we try and call it with more than one argument, we get an error. We separate arguments with commas.

double(2, 3)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[13], line 1
----> 1 double(2, 3)

TypeError: double() takes 1 positional argument but 2 were given
double(2, 3, 4)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[14], line 1
----> 1 double(2, 3, 4)

TypeError: double() takes 1 positional argument but 3 were given

Function arguments are expressions#

Remember that Python knows that the arguments to a function are expressions, and evaluates them, before running the function.

For example:

double(2 + 3)
10

All the procedure is the same as above. Python evaluates the expression 2 + 3, to get CR of int 5, then goes into function world, sets x to have the value CR of int 5, and continues from there.

Functions can have many arguments#

Now we define a new function:

def multiply(a, b):
    return a * b

The new thing here is that the function signature (a, b) has two arguments, separated by commas. We need to give the function two values, when we call it:

multiply(2, 3)
6

If we do not give it exactly two arguments, we get an error.

multiply(2)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[18], line 1
----> 1 multiply(2)

TypeError: multiply() missing 1 required positional argument: 'b'
multiply(2, 3, 4)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[19], line 1
----> 1 multiply(2, 3, 4)

TypeError: multiply() takes 2 positional arguments but 3 were given

Functions can have no arguments#

Perhaps the recipe analogy breaks down here, but sometimes functions take no arguments. For example:

import numpy as np
# Make random number generator.
rng = np.random.default_rng()
# Notice - nothing between the parentheses

def biased_coin():
    # A single random number
    r = rng.uniform()
    # A biased coin
    result = r < 0.45
    return result

When we call the function, we have no arguments, so no expressions between the parentheses.

biased_coin()
False

As you would expect by now, if we try and send an argument, Python will complain:

biased_coin(0.45)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[23], line 1
----> 1 biased_coin(0.45)

TypeError: biased_coin() takes 0 positional arguments but 1 was given

Without a return statement, functions return None#

Our functions so far all have a return statement. This is not true of every function.

If your function does not have a return statement, the function returns the value None.

def silent_addition(first, second):
    result = first + second

Notice that the body of this function has no return statement. When we call it, it returns None:

result = silent_addition(10, 12)
result
result is None
True

End of the introduction#

That’s it for the introduction. For a less basic description, have a look at the Berkeley introduction to functions.