Lesson A8 – Functions¶
Throughout the previous parts of this course we encountered many functions already. We know that a function is an object that can be called, that arguments can be passed to it and that functions return other objects.
returned_object = fxn(argurments)
You might have noticed, that we sometimes pass arguments directly. We call these arguments positional arguments.
[1]:
print("positional argument")
positional argument
Positional arguments are often required arguments. The order of positional arguments passed matters.
[2]:
range() # Needs a positional argument
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-b1150d557b1d> in <module>
----> 1 range() # Needs a positional argument
TypeError: range expected 1 arguments, got 0
[3]:
range(10) # One argument is interpreted as stop
[3]:
range(0, 10)
[4]:
range(1, 10) # Two arguments are interpreted as start, stop
[4]:
range(1, 10)
On the other hand we used some other arguments explicitly with a name. We call these arguments keyword arguments. These arguments are optional and their order does not matter, since they are passed with a name. Keyword arguments always need to come after all positional arguments.
[5]:
print("positional and keyword arguments", end="!")
positional and keyword arguments!
Note: We distinguish function arguments as positional arguments (required and interpreted by order) and keyword arguments (optional and identified by name).
We will now see how to write our own functions and how to control their behaviour by specifying their arguments. The purpose of functions is to bundle blocks of code in reusable objects under a label for identification. After loops, functions are our main tool to execute code many, many times that we have only written once.
Function definitions¶
Besides using functions provided by Python modules, we can write our own functions. The def
directive defines a code block as a function.
[6]:
def our_first_function():
"""This is our first function"""
print("Hey,")
print("this is our first function!")
Our function does not expect any arguments: the parenthesis in the above definition are empty. Calling it, executes the function body. Obviously a function needs to be defined first, before we can call it.
[7]:
our_first_function()
Hey,
this is our first function!
The call of our_first_function
printed a message to the screen. It did, however, not return a result. Precisely, if we try to store the return value of the function into a variable, we obtain None
.
[8]:
returned = our_first_function()
print(returned)
Hey,
this is our first function!
None
The print output to the screen is a side-effect of our function. Often we want functions to return something to the main program (e.g. the result of a calculation). We implement this with a return
statement in the function definition.
[9]:
def return_one():
"""Always describe your function here
Returns:
1
"""
return 1
[10]:
returned = return_one()
print(returned)
1
Advanced: A function that does not return anything explicitly, returns None
by default. Leaving out the return
statement within a function definition is equivalent to a bare return
or an explicit return None
.
def no_return():
pass # do nothing
def bare_return():
return
def explicit_return():
return None
Once a return is encountered during the execution of the function, we exit the function. Only the first return statement met will have an effect. Everything after a return statement is not executed.
[11]:
def return_this_or_that():
if True:
return True
return False
return_this_or_that()
[11]:
True
If we want to make our function handle arguments, we need to define what it expects in the first line of our definition. We put the argument within the parenthesis, we have left empty so far.
[12]:
def repeat_print(s, i=2):
"""Repeated calls of `print()`
Args:
s (str): String object to print
i (int): How often to print
Returns:
None
"""
for _ in range(i):
print(s)
[13]:
# Call of repeat_print
repeat_print("Hello!")
Hello!
Hello!
We use a required positional argument s
here and an optional keyword argument i
. When we call this function we need to pass at least one argument to it. The string "Hello!"
we passed is interpreted by position as a value for s
. Within the function, s
is now defined as s = "Hello!"
. By default, if we do not pass anything else, i
is set to i = 2
(two repeated prints). Optionally we can also set the parameter i
to a different integer. In this case we need to pass
it by name (i=3
) after the positional argument.
[14]:
# Call of repeat_print
repeat_print("Hi!", i=3)
Hi!
Hi!
Hi!
Note, that it is always a good idea to document what your function does with a comment directly at the top of the function body. These comments, called docstrings are also used when you call help()
on your function.
[15]:
help(repeat_print)
Help on function repeat_print in module __main__:
repeat_print(s, i=2)
Repeated calls of `print()`
Args:
s (str): String object to print
i (int): How often to print
Returns:
None
Let’s use what we have learned about functions for a more useful example now. When we talked about “Working with numbers” in an earlier exercise, we used Python to compute a Coulomb interaction between charges at a given distance. At this point we needed to change the parameters manually, if we wanted to get the result for a different value, e.g. another distance. A function can help us here to make the calculation convenient for any given value.
[16]:
def get_force_coulomb(r, q1=1.602e-19, q2=1.602e-19, k=9e9):
"""Compute Coulomb interaction
Uses $F = k \frac{q_1 q_2}{r^2}$
Args:
r (float): Distance (m)
Keyword args:
q1, q2 (float): Charges of interacting particles (C).
Defaults to elemental charges.
k (float): Force constant of the medium (N m$^2$ C$^{−2}$).
Defaults to vacuum.
"""
return (k * q1 * q2) / r**2 # Calculate force
Now we can calculate the interaction for a whole range of values and in other contexts without much code re-writing.
[17]:
# Generate 10 values in the open interval (0.1, 1) nm
r = [r * 0.1 * 1e-9 for r in range(1, 11)]
for r_ in r:
print(f"{r_:.2e} m: {get_force_coulomb(r_):.2e} N")
1.00e-10 m: 2.31e-08 N
2.00e-10 m: 5.77e-09 N
3.00e-10 m: 2.57e-09 N
4.00e-10 m: 1.44e-09 N
5.00e-10 m: 9.24e-10 N
6.00e-10 m: 6.42e-10 N
7.00e-10 m: 4.71e-10 N
8.00e-10 m: 3.61e-10 N
9.00e-10 m: 2.85e-10 N
1.00e-09 m: 2.31e-10 N
Advanced¶
Scopes¶
When we deal with functions we need to beware of scopes, that means we need to know about which variable is defined where. So far, when we assigned a value to a variable, this variable could be accessed from everywhere in our Python session. We say, they were globally defined or global variables. When we define variables within functions, however, these are only valid within this function. The variable s
for example used within our repeat_print
function, has no meaning outside this
function.
[18]:
print(s)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-18-0ff1b7208845> in <module>
----> 1 print(s)
NameError: name 's' is not defined
While we can access global variables from within functions, variables created within functions are only locally available.
[19]:
def use_global():
print(x)
x = 1
use_global()
1
[20]:
def use_local():
y = 1
print(y)
use_local()
print(y)
1
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-20-c6e3a9c81e29> in <module>
4
5 use_local()
----> 6 print(y)
NameError: name 'y' is not defined
Be careful with the use of global variables within your functions! When a function relies on a global variable it is more dependent on the context in which it is used and less re-usable in other situations. As a rule of thumb, a proper function should receive everything it needs as an argument and return the outcome of its execution as a return value. Side-effects as modifying global variables should be avoided if possible to make your functions clean and general.
Advanced: There are of course cases where global variables may be useful, for example when you have program-wide physical constants or settings defined. Be aware, that there is, however, no (easy) way to protect global data from changing in Python. Consider the use of local variables first, before you make your functions depend on globals. Passing a big data structure to a function to be modified locally is unproblematic, by the way, as Python passes objects essentially by reference. There is no copying of data involved and basically no performance issue with using local variables over globals.
When we define a local variable within a function, that has been also defined globally, the local value takes precedence. Precisely, Python creates a local variable and uses this instead of the global one.
[21]:
def use_local_instead_of_global():
x = 2
print(x)
x = 1
use_local_instead_of_global()
print(x)
2
1
A common mistake, leading to a strange error, is to try to modify a globally defined object from within a function with a statement that includes an assignment.
[22]:
def cant_do_this():
string = string.replace("ABC", "XYZ")
string = "ABC"
cant_do_this()
print(string)
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
<ipython-input-22-276a5619292d> in <module>
3
4 string = "ABC"
----> 5 cant_do_this()
6 print(string)
<ipython-input-22-276a5619292d> in cant_do_this()
1 def cant_do_this():
----> 2 string = string.replace("ABC", "XYZ")
3
4 string = "ABC"
5 cant_do_this()
UnboundLocalError: local variable 'string' referenced before assignment
In this case the string =
statements tells Python to create the local variable string
. But at the same time string.replace()
requires that string
already exists. While string
exists globally, we just said that we want to use string
as a local variable. Python can not handle this and raises an error. You can, however, modify global objects from within functions without local assignment:
[23]:
def can_do_this():
list_.append(1)
list_ = []
can_do_this()
print(list_)
[1]
You can also circumvent the local/global misunderstanding of Python and avoid the UnboundLocalError
by stating explicitly to use the global object. In this case Python does not create a local variable string
on assignments.
[24]:
def now_can_do_this():
global string
string = string.replace("ABC", "XYZ")
string = "ABC"
now_can_do_this()
print(string)
XYZ
Note: It is sometimes helpful in general to avoid naming conflicts between global or local variables in the first place, e.g. by choosing generic place-holders as arguments to functions and descriptive names for variables outside functions. In our get_force_coulomb
example we used r
in three different roles: as local variable, as global sequence, and as control variable in a list comprehension. We could have chosen three distinct names to make their role more clear.
You can also choose to mark global variables with a leading underscore (_globvar
) or to put variables supposed to be static constants in all uppercase (OUTPUT
). The naming convention section of PEP8 is a good standard reference if you want to learn more.
Scopes can be confusing at times, in particular when scopes are nested. The global scope is always the outermost layer in which variables can be defined (the session- or module-level). The local scope is always the innermost layer, enclosed by the lowest level function.
[25]:
def demonstrate_scopes():
x = "I am local to 'demonstrate_scopes'"
def modify_global():
global x
x = "I was changed by 'modify_global'"
y = "I am local to 'modify_global'"
print(y)
print(x)
modify_global()
x = "I am global"
print(x)
demonstrate_scopes()
print(x)
I am global
I am local to 'demonstrate_scopes'
I am local to 'modify_global'
I was changed by 'modify_global'
Between the local and the global layer, Python does also know the non-local layer. It addresses the next higher enclosing scope seen from the current local scope. You may not need this anytime soon in practice but it helps to understand scopes and where variables are valid. Let’s end this tutorial with an advanced example on this.
[26]:
def demonstrate_scopes():
x = "I am local to 'demonstrate_scopes'"
def modifiy_non_local():
nonlocal x
x = "I was changed by 'modify_non_local'"
def modify_global():
global x
x = "I was changed by 'modify_global'"
print(x)
modifiy_non_local()
print(x)
modify_global()
print(x)
x = "I am global"
print(x)
demonstrate_scopes()
print(x)
I am global
I am local to 'demonstrate_scopes'
I was changed by 'modify_non_local'
I was changed by 'modify_non_local'
I was changed by 'modify_global'