Functions
So far in this course we’ve encountered a few basic functions such as print, sum, len, and some others from the math module. However, we’ve largely glossed over the details of what functions are, how they work, and how we might create our own functions.
Function basics
A function is a piece of reusable code that takes some input(s), manipulates it/them in a useful way, and (usually) returns some output(s).
We call a function by typing its name followed by a set of brackets into which we place the inputs. For example
message = 'Here we are calling the print function!'
print(message)We refer to the inputs to a function as arguments - just like in mathematics. So, in the above reminder, when we use the print function we would describe this more correctly as calling the print function with message passed as the only argument.
Some functions like print don’t return anything back to us - we don’t get any results or new variables. We’ve previously used the math module to obtain values of the cosine and sine functions, and in that case we are returned a single value corresponding to \(\cos\) or \(\sin\) evaluated at our specified angle. For example, \(\cos(\pi)\).
import math
math.cos(math.pi)-1.0
We can place the result of this call to math.cos in a variable using the assignment operator =.
import math
value = math.cos(math.pi)
print(f'cos(π) = {value:.1f}')cos(π) = -1.0
Defining functions
Now you’ve been reminded of how we call functions, let’s get on with writing some of your own.
To get started, we’ll use an example to take a look at the components that make up a function, and then in later sections we’ll delve into the specifics of how each works.
The following simple (and rather pointless!) add function sums two numbers
def add(a, b):
result = a + b
return resultThe first line starts with the def (short for define) keyword - this tells Python that we are defining a new function.
We then follow this with the name of our function - here we’ve chosen add.
Then we have a set of brackets containing the two arguments to the function - here we’ve called them a and b.
We follow this with the contents of the function - the actual code that will run when we call this function. Notice that the code is indented by one tab (or four spaces) - this tells Python that this code is part of the function.
In this function the variables a and b are added together to make the variable result. The return keyword is used to indicate what we’d like the output of the function to be. In this example we are returning the variable result.
To use this function we simply call it like any other, for example we can compute \(5+6\) by imputting two int values.
add(5, 6)11
but we could also pass a pair of variables
number_1 = 1.25
number_2 = 2.15
add(number_1, number_2)3.4
You might be wondering - what about a and b, why have we used number_1 and number_2? The answer to this question requires us to learn about something called scope.
Scoping
Functions are machines - they take an input and process it to give an output. Within the internal workings of a function there need to be a set of names for the input variables, output variables, and any other variables that the function might create and use when it is executed. We call this “list of names” the scope of the function.
Let’s see an example - a function which converts a frequency in Hertz to an energy in Joules.
def freq_to_energy(freq_hz):
planck = 6.626E-34 # J s
# Energy in Joules
# E = h * nu
energy_j = freq_hz * planck
return energy_jThe variable planck is Planck’s constant and is used to convert from frequency to energy.
Let’s use this on a frequency value, perhaps for an x-ray with \(\nu=5\times10^{17}\mathrm{\ Hz}\).
freq_to_energy(5E17)3.313e-16
Great! Now what happens if we try to print the planck variable?
freq_to_energy(5E17)
print(planck)Traceback (most recent call last):
File "<python-input-7>", line 2, in <module>
print(planck)
^^^^^^
NameError: name 'planck' is not definedBut, didn’t we just define the planck variable inside our function? Yes! But planck only exists within the function freq_to_energy - more specifically we say that this is a local variable which exists within the local scope of that function. For the same reason, printing the variables freq_hz and energy_j outside of freq_to_energy will give an error - they exist only within the local scope of freq_to_energy.
Each function has its own separate scope, allowing us to reuse variable names inside of functions without worrying about them already being defined, or changing definition.
So if a local scope describes the set of variables within a function, then you might wonder if there’s an equivalent name for everything that exists outside of functions? Yes, we call this the global scope - this is where all of the objects you’ve defined outside of functions reside.
For example, if you define a variable (outside of a function), e.g.
a = 5we say that it resides in the global scope of the program. Simple!
What can be slightly confusing is that objects in the global scope are also accessible inside of a function.
Here’s an example
number_ten = 10
def scope_example(a, b):
result = (a - b) / number_ten
return resultWe use the variable number_ten inside of our function but we’ve defined it elsewhere. This is what we call a global variable.
The use of global variables is extremely bad practice in python - your code will rapidly become unreadable and highly prone to errors.
To see why - use a new cell and redefine number_10 as the string '10', then call the function scope_example again - what happens?
There is one situtation in which global variables should not be looked upon with disdain - constants. In science we refer to fixed quantities as constants, for example the ideal gas constant \(R=8.314\, \mathrm{\ J\ K^{-1}\ mol^{-1}}\) is a constant. In Python we use a similar definition: constants are variables which remain fixed throughout the code - they’re defined once at the start of a program and used repeatedly.
Constants are usually denoted by CAPITAL LETTERS, but are not a new type of object - this is simply a style choice that tells us not to modify this variable.
As an example, we could define the number of seconds in an hour at the start of our program.
SECONDS_IN_HOUR=3600and then use this constant in a function
def hours_to_seconds(hours):
return hours * SECONDS_IN_HOURArguments
In our previous examples we’ve seen that we can have different numbers of arguments to a function. We can even have zero arguments
def zero_args_func():
print('This function takes zero arguments')
print('and returns nothing')
return
zero_args_func()'This function takes zero arguments'
'and returns nothing'
You still need to include brackets when calling a function without arguments - otherwise Python will treat your command as a variable name.
By default, arguments to a function are positional in nature - we have to give them in the order that the function expects. The following code executes successfully.
def print_name_and_age(name, age):
print(f'Your name is {name}, and you are {age:d} years old.')
print_name_and_age('Tim', 25)but what happens if we switch the order of the arguments? Below we’ve provided an age where the name should be and vice-versa.
print_name_and_age(25, 'Tim')Traceback (most recent call last):
File "<python-input-10>", line 1, in <module>
print_name_and_age(25, 'Tim')
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
File "<python-input-9>", line 3, in print_name_and_age
print(f'Your name is {name}, and you are {age:d} years old')
^^^^^^^
ValueError: Unknown format code 'd' for object of type 'str'In this case we see an error - because the print statement expects age to be an `int - and we can correct our call to the function to have the correct argument order.
What about if we only want to provide an argument some of the time. Then we can use a keyword argument which is optional and has a default value that is used if that keyword is not specified.
Consider the function below, we define moles as a keyword argument with a default value of 1.
def ideal_gas_pressure(volume, temperature, moles=1)
return moles * constants.R * temperature / volume
ideal_gas_constant(5, 298)
ideal_gas_constant(5, 298, moles=1)
ideal_gas_constant(5, 298, moles=25)The first two calls give the same value. In the first call we don’t specify a value for moles and so it defaults to 1, just as we defined. In the second call we set moles=1 and see the same result, and in the final call we set moles=25 and see a different result.
When defining a function, make sure that all positional arguments are written before any keyword arguments.
def correct(a, b, c=1)
return a*b*cis correct
but the following gives an error
def correct(a, b=1, c)
return a*b*cKeyword arguments are particularly useful when they are combined with booleans and logic - something we’ll cover in Session 4.
Return
Functions use the return keyword to specify what the output should be. For example, our add function returns the result of adding a and b
def add(a, b):
result = a + b
return resultbut what happens if we don’t return anything.
def add(a, b):
result = a + b
return
add(5, 10)Nothing! The calculation occurs, and is stored in result, but then the function ends, doesn’t return anything, and result is lost.
What happens if we print this
print(add(5, 10))None
or use the type function
type(add(5, 10))NoneType
so we recieve None - which is Python’s type for nothing or null and represents a lack of data.
This might seem useless, but there are situations where a function doesn’t need to return a value - print is one such function.
type(print('The print function does not return anything!'))The print function does not return anything!
NoneType
You can see this even easier with a variable
print_return_value = print('This will allow us to store whatever the print function returns!')
print(print_return_value)This will allow us to store whatever the print function returns!
None
We can also choose to return more than one output variable, for example this function returns the sum and difference of two numbers
def add_and_subtract(a, b):
addition = a + b
subtraction = a - b
return addition, subtraction
add_and_subtract(3, 9)12 -6
When more than one value is returned, the function is actually returning a tuple
result = add_and_subtract(3, 9)
print(result)
type(result)(12, -6)
tuple
We can access the elements of a tuple using indexing, as we learned in Session 2. Alternatively, we can use multiple assignment to store each value in a separate variable.
addition, subtraction = add_and_subtract(3, 9)
print(addition, subtraction)
print(type(addition), type(subtraction))12 -6
int, int
Why do we use functions?
In the first Session’s synoptic exercises, you calculated the wavelength of the first line in each of the Lyman, Balmer, and Paschen hydrogen emission series.
Notice in the answers that the same lines were repeatedly copied and pasted for each series, resulting in quite a messy piece of code with lots of repetition.
We could instead define a function which calculates the energy of a given transition between two states \(n_\mathrm{i}\) and \(n_\mathrm{f}\).
import scipy
def calc_rydberg_wavelength(n_i, n_f):
iwavelength = scipy.constants.Rydberg * (1/n_f**2 - 1/n_i**2)
wavelength = 1 / iwavelength
wavelength *= 10**9
return wavelengthThis could then be called for each transition to give a much more readable piece of code which doesn’t write the mathematical operations three times.
lyman_lambda = calc_rydberg_wavelength(2, 1)
balmer_lambda = calc_rydberg_wavelength(3, 2)
paschen_lambda = calc_rydberg_wavelength(4, 3)