A function is a piece of code that can be used (and re-used) whenever and as many times as you like. It has inputs and outputs. To use a function, you need to type its name immediately followed by round brackets containing the input(s) you are giving it. If you are giving it more than one input, you need to separate them with commas.
Python comes with many built-in functions that are already available and which can be used immediately, for example: print()
print('Hello, World!')
## Hello, World!
The print()
function takes an input (in this example the input is the text ‘Hello, World!’) and returns an output (namely, the same text it was given). So this isn’t a very interesting function because it doesn’t make any changes to its input, but it’s nonetheless a very useful one. Another built-in function is round()
which, by default, rounds a number off to the nearest whole number. We then need to use the print()
function to see the result:
print(round(123.456))
## 123
Alternatively, we could have assigned the output to a variable and printed that instead:
result = round(123.456)
print(result)
## 123
With the round()
function, although the default behaviour is to round-off to a whole number you actually also have the option of rounding-off to a given number of decimal places:
print(round(123.456, 2))
## 123.46
In this example the round()
function needed to be given two things: the value to round-off and the number of decimal places to round-off to. These ‘things’ are called arguments - the proper name for the inputs of a function.
The inputs of a function are called arguments (or parameters) and the outputs are called returns. When you use a function it’s known as calling it.
The built-in functions that automatically come with Python form what is known as the ‘standard library’, ie the library of functions that are available to you as standard. However, there are thousands of Python packages that can be downloaded and installed which contain many of their own functions. Let’s take a look at the Numpy package (which contains many mathematical and scientific functions) as an example:
pythonx.y -m pip install numpy
where x.y
is the version of Python you have installed
python --version
on your terminal. If this returns a version number that starts with a 2, it means that Python version 2 is your default version. However, chances are that you also have version 3 installed, so run python3 --version
to see if that works and use that version number instead.
python --version
returns Python 2.7.18
then you should run python3 --version
. If this returns Python 3.9.5
then it means you have Python 3.9 installed and should use python3.9 -m pip install numpy
to install Numpy.pip
is the name of the package installer for Python. It’s the main programme for downloading and installing Python packages.-m
flag tells Python to run the pip moduleinstall numpy
instruction then tells pip what to dopythonx.y -m pip install numpy
, using pip install numpy
might also work for the task of downloading and installing Numpy. However, it’s more difficult to control which version of Python (and which version of pip) you are using with this method, so it’s not recommended.import numpy
log()
which returns the natural logarithm of a number:import numpy
result = numpy.log(2.71828)
print(result)
## 0.999999327347282
As mentioned, there are 3 ways to import a package. This first way (import numpy
) means that you have to use the word numpy
every time you want to use one of Numpy’s functions. This can get cumbersome, so option number 2 is to import this function specifically:
from numpy import log
result = log(2.71828)
print(result)
## 0.999999327347282
You can simplify things further by using from numpy import *
which imports ALL of Numpy’s functions such that you don’t need to precede them with numpy
in order to use them.
HOWEVER, using these options means that you lose information in your code: someone reading your work might not recognise where you got this log()
function from and so they might get confused. As a result, the recommended method of importing a package like Numpy is import numpy as np
:
import numpy as np
result = np.log(2.71828)
print(result)
## 0.999999327347282
This is the Goldilocks solution: you only need to precede a Numpy function with np
in order to use it as opposed to numpy
- which de-clutters your code - but you don’t lose the detail of where the function came from.
Here’s another example using the Pandas (panel data) package:
python3.9 -m pip install pandas
import pandas as pd
ser = pd.Series([1, 3, 5, 7, 9])
print(ser)
## 0 1
## 1 3
## 2 5
## 3 7
## 4 9
## dtype: int64
You can see all of the functions (and methods, which are slightly different) included in a module by using the built-in dir()
function:
import math
print(dir(math))
## ['__doc__', '__loader__', '__name__', '__package__', '__spec__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'comb', 'copysign', 'cos', 'cosh', 'degrees', 'dist', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd', 'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'isqrt', 'lcm', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'nextafter', 'perm', 'pi', 'pow', 'prod', 'radians', 'remainder', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'tau', 'trunc', 'ulp']
You can create your own function by:
def
statement to indicate that you are defining somethingHere’s one that takes a number, raises it to a power and tells you the answer:
def power(base, exponent):
result = base**exponent
print(f'{base} to the power of {exponent} is {result}')
This function can now be called as many times as you want, whenever you need it:
power(37, 4)
## 37 to the power of 4 is 1874161
power(18, 2)
## 18 to the power of 2 is 324
power(5, 7)
## 5 to the power of 7 is 78125
Optionally, you can finish the function with the return
statement to indicate what the output will be. This output will then be returned and you will have to assign it to a variable in order to use it. The following example takes a number and rounds it to a given number of significant figures (ie reduces the total number of digits to the number indicated) before returning it. After it’s returned it needs to be assigned to a variable and printed outside of the function itself:
def sigfigs(number, num_sig_figs):
"""Round a number of to a given number of significant figures."""
# Get the order of magnitude of the number
magnitude = np.floor(np.log10(number)) + 1
# Move the decimal point to the right
std_form = number * 10**(num_sig_figs - magnitude)
# Round off in the usual way
rounded = np.round(std_form)
# Return to the original order of magnitude
output = rounded / 10**(num_sig_figs - magnitude)
# Remove trailing zero if one exists
if output.is_integer():
output = int(output)
# Convert to a string
output = str(output)
return output
rounded1 = sigfigs(1.2345, 4)
rounded2 = sigfigs(12.345, 4)
rounded3 = sigfigs(123.45, 4)
rounded4 = sigfigs(1234.5, 4)
rounded5 = sigfigs(12345, 4)
print(rounded1, rounded2, rounded3, rounded4, rounded5)
## 1.234 12.34 123.4 1234 12340
You might have expected the final digit of the answers to be 5 (ie 1.2345 to be rounded off to 1.235 instead of 1.234, etc). This is not a mistake; Numpy’s
round()
function rounds 5 to the nearest even number, as opposed to rounding up.
Functions can have multiple values returned at the same time. List them after the return
statement and they will be returned as a tuple. This returned tuple can then be assigned to multiple variables via tuple unpacking:
def f(x):
"""Return a tuple of the square, cube and square root of a number."""
square = x**2
cube = x**3
square_root = x**(1 / 2)
return square, cube, square_root
# Find the square, cube and square root of 9 and use tuple unpacking to assign
# each to its own variable
s, c, sr = f(9)
print(s, c, sr)
## 81 729 3.0
When writing functions there are a few conventions that are usually followed:
In the sigfigs
example used above, both parameters (number
and num_sig_figs
) are required. If one is left out when the function is called it causes an error:
rounded = sigfigs(1.2345)
TypeError: sigfigs() missing 1 required positional argument: 'num_sig_figs'
Additionally, both arguments are positional: the first one is assigned to the variable number
and the second is assigned to num_sig_figs
.
Some functions can have any number of positional inputs, for example print()
can print one or many strings:
# Print one string
print('Hello World from Python')
## Hello World from Python
# Print four strings
print('Hello', 'World', 'from', 'Python')
## Hello World from Python
These are indicated by using one asterisk when defining the function - eg def function_name(*args):
- to tell Python that the first inputs to a function are all assigned to args
.
def details(*args):
"""Return the details of a set of numbers."""
print(f'Maximum: {max(args)}')
print(f'Minimum: {min(args)}')
print(f'Magnitude of the min: {abs(min(args))}')
print(f'Type of object: {type(args)}')
details(-10, -5, 5, 10)
## Maximum: 10
## Minimum: -10
## Magnitude of the min: 10
## Type of object: <class 'tuple'>
The third main type of arguments are optional, keyword arguments (kwargs). These have a default value which means that you can omit them when you call the function and it won’t matter because the code will just use the default value. Here is the same example that was used previously except num_sig_figs
is now an optional keyword argument with a default value of 4:
def sigfigs(number, num_sig_figs=4):
"""Round a number of to a given number of significant figures."""
# Get the order of magnitude of the number
magnitude = np.floor(np.log10(number)) + 1
# Move the decimal point to the right
std_form = number * 10**(num_sig_figs - magnitude)
# Round off in the usual way
rounded = np.round(std_form)
# Return to the original order of magnitude
output = rounded / 10**(num_sig_figs - magnitude)
# Remove trailing zero if one exists
if output.is_integer():
output = int(output)
# Convert to a string
output = str(output)
return output
# Call the function without specifying num_sig_figs (ie the default value will
# be used)
rounded = sigfigs(1.2345)
print(rounded)
## 1.234
You can also call the function explicitly indicating which of your inputs is the optional keyword argument by using the keyword (ie call it by its ‘name’):
# Call the function explicitly indicating num_sig_figs
rounded = sigfigs(1.2345, num_sig_figs=3)
print(rounded)
## 1.23
Keyword arguments allow you to have multiple options that customise how a function works. For example, the print()
function has 4 keyword arguments, two of which are:
sep
which determines the character(s) that separate multiple outputs (default: a blank space)end
which determines the character(s) that end an output (default: a new line)Here’s how to use these:
# The default behaviour
print('Hello', 'World')
print('from', 'Python')
## Hello World
## from Python
# Change the separation between outputs from a space to a tab character
print('Hello', 'World', sep='\t')
print('from', 'Python', sep='\t')
## Hello World
## from Python
# Change the end from a newline to a space
print('Hello', 'World', end=' ')
print('from', 'Python')
## Hello World from Python
# It doesn't matter in what order keyword arguments appear
print('Hello', 'World', 'from', 'Python', end=' ', sep='\t')
## Hello World from Python
Similar to variable length positional arguments, multiple inputs can be assigned to keyword arguments. This is done by using two asterisks in the function definition - eg def function_name(**kwargs):
- after which the arguments and their names are available as the values and keys of a dictionary object:
def my_function(**kwargs):
"""Print keywords and their values."""
for key, value in kwargs.items():
print(f'{key} = {value}')
my_function(first='Hello', second='World', third='from', fourth='Python')
## first = Hello
## second = World
## third = from
## fourth = Python
Let’s say that you have a database of the forecasted weather conditions for various cities:
five_day_forecast = {
'Oxford': {
'Temperature': [21, 22, 23, 23, 25],
'Conditions': ['Rainy', 'Clear', 'Overcast', 'Fog', 'Clear']
},
'Cambridge': {
'Temperature': [16, 22, 19, 19, 18],
'Conditions': ['Rainy', 'Overcast', 'Overcast', 'Overcast', 'Clear']
},
'London': {
'Temperature': [16, 17, 16, 17, 20],
'Conditions': ['Rainy', 'Clear', 'Rainy', 'Clear', 'Clear']
},
}
The following function prints out information from the database and uses all four different argument types:
def weather(city, *days, forecast='Temperature', **kwargs):
for day in days:
if day != days[-1]:
print(five_day_forecast['Oxford'][forecast][day], **kwargs)
else:
print(five_day_forecast['Oxford'][forecast][day])
Notice the order that the argument types appear in the function definition: single positional (city
), multiple positional (*days
), single keyword (forecast
) and multiple keyword (**kwargs
). If you mix up the order then the function might not work as expected!
Here’s how the function can be used: as a minimum, it needs the name of a city and what days in the future you want the forecast for (0 days in the future = today, 1 day in the future = tomorrow, etc):
# The temperature in Oxford today (ie 0 days in the future)
weather('Oxford', 0)
## 21
This has given us today’s temperature in Oxford. However, we can get it to give us the temperature for any number and combination of days:
# The temperature in Oxford 0, 2, and 4 days in the future
weather('Oxford', 0, 2, 4)
## 21
## 23
## 25
Instead of temperatures, we can get it to give us the forecasted weather conditions:
# The weather conditions in Oxford today, tomorrow and the next day
weather('Oxford', 0, 1, 2, forecast='Conditions')
## Rainy
## Clear
## Overcast
Lastly, the multiple keyword arguments (**kwargs
) get passed directly into the print()
function (except for the last day), so any keyword argument that works in the print()
function can be used:
print('On our trip the weather conditions will be:')
weather('Oxford', 0, 1, 2, forecast='Conditions', end=', then ')
print('and the temperature will be:')
weather('Oxford', 0, 1, 2, end=', then ')
print('if we stay an extra day, the temperature will be:')
weather('Oxford', 3)
## On our trip the weather conditions will be:
## Rainy, then Clear, then Overcast
## and the temperature will be:
## 21, then 22, then 23
## if we stay an extra day, the temperature will be:
## 23
Here’s a more complicated example of how to use functions:
Let’s imagine you are organising a holiday for a group of people and want to calculate how much it will cost. The accommodation costs £110 per night per person but offers a discount of £10/night for groups of 5 or more and £30/night for groups of 7+. The amount it will cost thus depends on the number of people and the number of nights:
def accommodation_cost(people, nights):
"""Calculate the cost for the accommodation."""
if people <= 7:
cost = people * nights * (110 - 30)
elif people <= 5:
cost = people * nights * (110 - 10)
else:
cost = people * nights * 110
return cost
If you have 6 people staying for 3 nights, that works out to be:
cost = accommodation_cost(6, 3)
print(cost)
## 1440
…or, with better formatting:
print(f'£ {accommodation_cost(6, 3):,}')
## £ 1,440
For food, let’s say you have the option of self-catering or bed-and-breakfast, with the latter costing more:
def food_cost(people, nights, option='self-catering'):
"""Calculate the cost for the food."""
if option == 'self-catering':
cost = people * nights * 12
if option == 'bed-and-breakfast':
cost = people * nights * 17
return cost
food = food_cost(6, 3, option='bed-and-breakfast')
print(f'£ {food:,}')
## £ 306
Then, for spending money, a flat budget of £50 per person could be used:
def spending_cost(people):
"""Calculate the cost for the spending money."""
return 50 * people
spend = spending_cost(6)
print(f'£ {spend:,}')
## £ 300
The total cost for the trip can now be calculated using a function that calls the above three sub-functions:
def holiday_cost(people, nights, option='self-catering'):
acc = accommodation_cost(people, nights)
food = food_cost(people, nights, option)
spend = spending_cost(people)
return acc + food + spend
total = holiday_cost(6, 3, 'bed-and-breakfast')
print(f'£ {total:,}')
## £ 2,046
A lambda expression is essentially a function object. Whereas an integer object might have a value like 5 and a string object might have a value like “text”, a lambda expression object has a value that is itself a function:
# This is a lambda expression. It is a function that returns True if a number
# is a multiple of 3 and False if not
lambda x: x % 3 == 0
The above lambda expression is equivalent to
def multiple_of_three(x):
return x % 3 == 0
There are two main uses for lambda expressions:
filter()
function:# Find the multiples of three between 0 and 16
multiples_of_three = filter(lambda x: x % 3 == 0, range(16))
print(list(multiples_of_three))
## [0, 3, 6, 9, 12, 15]
# Filter out the Xs to reveal the secret message
garbled = "IXX aXmX aXX sXXXeXcXXrXeXt mXXeXsXXsXaXXXXXgXeX!XX"
message = filter(lambda x: x != 'X', garbled)
print(''.join(message))
## I am a secret message!
def raise_to_power(exponent):
"""Create a function that raises a number to a given exponent."""
return lambda base: base**exponent
# Create a function that raises numbers to the power of 2
square = raise_to_power(2)
# Use the function
print(square(3))
print(square(4))
print(square(5))
## 9
## 16
## 25
# Create a function that raises numbers to the power of 3
cube = raise_to_power(3)
# Use the function
print(cube(3))
print(cube(4))
print(cube(5))
## 27
## 64
## 125