6.3 Functions in more detail
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?
- Python finds what
double
points to. It points to internal representation of our function (procedure). - Next it sees the parenthesis
(
and sees that we want to call our function. - 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. -
Now Python does the call. To do this it:
- Puts itself into function world (more on this later).
- Sets the new variable
x
to have the value CR of int 4, from above. - Executes the code in the function body (procedure).
- The first line
d = x * 2
is an assignment statement.x
evaluates to CR of int 4, 2 evaluates to CR of int 2, sod
has the value CR of int 8. This is how the statement would work anywhere in Python, function body or not. - The next line starts with
return
. This is a return statement. When Python sees areturn
statement, it evaluates the expression to the right, to get the return value, then - Pulls itself out of function world.
- 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)
...
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)
...
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)
...
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)
...
TypeError: double() takes 1 positional argument but 2 were given
double(2, 3, 4)
TypeError Traceback (most recent call last)
...
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)
...
TypeError: multiply() missing 1 required positional argument: 'b'
multiply(2, 3, 4)
TypeError Traceback (most recent call last)
...
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
# Notice - nothing between the parentheses
def biased_coin():
# A single random number
r = np.random.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)
...
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.
Now try the exercises.