7. Functions#
Kristaps Stolarovs and Afiq Bin Nor Kamil
kristaps.stolarovs@manchester.ac.uk
Department of Mechanical and Aerospace Engineering, The University of Manchester, Manchester, UK
7.1. Intended Learning Outcomes#
After reading this page, you should be able to:
Describe the basic definition and properties of functions in programming.
Define and call simple functions.
Provide arguments and return outputs from functions.
Implement lambda functions.
Implement recursive functions.
Document your functions.
7.2. What are functions?#
You can pass parameters (data), into a function, with the function returning some processed data as a result.
You are aready familiar with mathematical functions:
In programming, we refer to the input \(x\) as a parameter, whilst the output \(f(x)\) as the return value.
In Python, such a function would look something like this:
def fun(x):
fx = x**2 + 4
return fx
If we wanted to evaluate the function like so:
We would pass the argument 5 to the parameter x. We could do it like so:
fx = fun(5)
So, to clarify:
A parameter is the variable listed inside the parentheses in the function definition.
An argument is the value that are sent to the function when it is called.
Here is the full code:
def fun(x):
fx = x**2 + 4
return fx
fx = fun(5)
print(fx)
29
In programming however, functions are not limited to just mathematical operations. They can encapsulate any piece of code you can write. They can have any number of parameters (including none), and any number of return values (also including none). Here we will explore the application of said functions and their variations.
7.3. Variable scopes#
Before continuing, it is recommended that you re-familiarise yourself with the concept of variable scopes, the description of which can be found in the Variables & Types chapter.
7.4. Motivation#
Functions are a somewhat abstract concept in programming, but this example might help understanding why they are important:
Imagine you’re a developer working on the university’s course management system.
There are hundreds of courses that the university provides, each comprising different modules.
Many of the courses may share common functionalities, such as enrollment, grading, and scheduling.
Instead of writing the same code for these tasks in every course module, you can create functions to handle these operations once and reuse them across multiple courses.
Functions in Python are essential because they allow you to encapsulate and reuse code, improve code organization, and enhance overall efficiency.
7.5. Defining, calling functions#
As you already saw, functions are defined using the keyword def. The basic syntax is pretty simple. The function name follows the def keyword. Function names, just like variables, should be descriptive and follow the Python PEP 8 style guide. They also have the same naming restrictions as variables (no spaces, only alphanumeric characters and underscore, etc.). The function name is directly followed by the rameters, which are enclosed in a pair of parenthesis, after which follows a colon.
The next lines will contain your function’s code. It is important to note that unlike other languages, indentation in Python is very important, and is used by the interpreter to determine the intended scope of the code you write. Because of this, any code inside a function must be indented.
Once you have written the code you intend for your function to encapsulate, you may then return any values you would like using the return keyword, followed by the variable names of the values you would like to return.
It is important to note that you do not have to provide any arguments or extract any output values from a function if you do not want to.
Putting it all together, you might expect your function to look something like this:
def function_name(input_1, input_2, ...):
#
# some code
#
return output_1, output_2, ...
You can then call your function at any point below the function’s definition by simply calling the function, like so:
output_1, output_2, ... = function_name(input_1, input_2, ...)
7.6. Arguments#
As already mentioned, function areguments are used to provide data to a function. Arguments are specified inside the parentheses when defining a function. You may add any number of arguments, with each argument separated by a comma. If you have not defiend any default argument values, you must call the function with the correct number of arguments.
There are several types of function arguments in Python.
7.6.1. Positional arguments#
These are the most common type of arguments. Their value is assigned based on their position in the argument list.
7.6.1.1. Example:#
def div(a, b):
result = a / b
print('Answer:', result)
div(10, 5)
div(5, 10)
Answer: 2.0
Answer: 0.5
This works the same way if you are passing variables as arguments:
x = 9
y = 3
div(x, y)
div(y, x)
Answer: 3.0
Answer: 0.3333333333333333
7.6.2. Keyword arguments#
You can however explicitly specify the intended parameter and its value. In this case, the ordering of the arguments does not matter, and will always give the same result. The phrase “Keyword Arguments” is often shortened to kwargs in Python documentations.
7.6.2.1. Example:#
div(a=10, b=5)
div(b=5, a=10)
Answer: 2.0
Answer: 2.0
div(a=x, b=y)
div(b=y, a=x)
Answer: 3.0
Answer: 3.0
7.6.3. Default arguments#
Default arguments allow for default values to be specified in the function definition. Using this functionality, you can call a function without passing an argument if the parameter has a default argument. If however a specific argument value for the parameter is inputted, then that value will be used instead.
7.6.3.1. Example:#
def div(a=5, b=10):
result = a / b
print('Answer:', result)
div() # Here, a is 5 and b is 10
div(10) # Here, a is 10 and b is 10
div(b=2) # Here, a is 5 and b is 2
Answer: 0.5
Answer: 1.0
Answer: 2.5
7.6.4. Argument types#
You can pass any data type as an argument, whether it is an int, list, string, etc.
7.7. Returning values#
So far the code examples have not included the return keyword. When a function doesn’t include a return statement, it implicitly returns None. We can see this behavior here:
example = div()
print(example)
Answer: 0.5
None
If we try to unpack multiple values from a function that returns either None or a different number of values, we’ll get a TypeError:
example_1, example_2 = div()
Answer: 0.5
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Cell In[8], line 1
----> 1 example_1, example_2 = div()
TypeError: cannot unpack non-iterable NoneType object
7.7.1. Example:#
Here’s how we can write functions that return single or multiple values:
def div_1(a=5, b=10):
result = a / b
return result # Returns a single value
example_1 = div_1()
print(example_1)
0.5
def div_2(a=5, b=10):
result = a / b
return result, a, b # Returns multiple values
example_1, example_2, example_3 = div_2()
print(example_1, example_2, example_3)
0.5 5 10
When a function returns multiple values, we can either unpack them into separate variables as shown above, or store them as a tuple:
example_tuple = div_2()
print(example_tuple)
print(type(example_tuple))
(0.5, 5, 10)
<class 'tuple'>
7.8. Lambda functions#
Lambda functions are small, anonymous function that are defined using the lambda keyword instead of def, followed by a list of arguments, a colon, and an expression. They can take any number of arguments but can only have one expression with one output. These are most frequently used when you need to pass a function as an argument to another function. They serve to keep logic contained to where it is being used, avoiding cluttering your code with single-use definitions.
7.8.1. Example:#
Say we wanted to sort string elements in an array in order of length.
Without lambda - we’d need a separate function definition:
def get_length(x):
return len(x)
words = ["lambda", "numerical methods", "function"]
sorted_words = sorted(words, key=get_length)
print(sorted_words)
['lambda', 'function', 'numerical methods']
With lambda, the code is more concise and the intent clearer:
words = ["lambda", "numerical methods", "function"]
sorted_words = sorted(words, key=lambda x: len(x))
print(sorted_words)
['lambda', 'function', 'numerical methods']
7.9. Recursion#
A recursive function is one that calls itself as part of its execution. Each recursive function needs two main components:
A base case that stops recursion
A recursive case that breaks the problem into smaller subproblems
The classic example that you would have run into is calculating a factorial. For a number \(n\), its factorial (\(n!\)) is a product of all positive integers, from \(1 \rightarrow n\). We can write this recursively in Python like so:
def factorial(n):
if n == 1:
return 1 # Base case
else:
return n * factorial(n-1) # Recursive case
print(factorial(5))
120
Each recursive call creates a new instance of the function with its own local variables. These instances are stored in what’s called the “call stack”. The function calls continue until they reach the base case, then the results are passed back up the stack to compute the final result.
Another common example is the Fibonacci sequence, where each number is the sum of the previous two:
def fibonacci(n):
# Base cases
if n <= 0:
return 0
if n == 1:
return 1
# Recursive case
return fibonacci(n - 1) + fibonacci(n - 2)
While recursion can make code more elegant and easier to understand for certain problems (especially those with a naturally recursive structure like tree traversal), it’s important to note that recursive solutions can be less efficient than iterative ones due to the overhead of multiple function calls and stack space usage. Additionally, if you implement your function incorrectly, you can introduce infinite loops, which will cause either your execution environment or whole system to crash.
7.10. Documenting functions#
When you are writing functions, it is very important to make sure that other people (this includes future you!) are able to discern how your function works and how to use it. Python offers several ways to document functions, making your code more maintainable and easier for others to use. The main approaches are docstrings and type hints.
7.10.1. Docstrings#
A docstring (documentation string) is a text description enclosed in triple quotes (""") that appears at the start of a function. The triple quotes allow us to write text that spans multiple lines. This text describes what the function does, its parameters, and what it returns:
def calculate_bmi(weight, height):
"""Calculate Body Mass Index (BMI) from weight and height.
Args:
weight (float): Weight in kilograms
height (float): Height in meters
Returns:
float: BMI value (kg/m²)
Example:
>>> bmi = calculate_bmi(70, 1.75)
>>> print(bmi)
22.86
"""
return weight / (height ** 2)
7.10.2. Type Hints#
Type hints were introduced in Python 3.5 to help programmers specify what kind of data a function expects to receive and return. While Python won’t enforce these hints (you can still pass different types), they help prevent errors and make code easier to understand. You can specify the input argument type by adding a colon, space and then the intended type following a parameter’s name. You can then specify the output argument value by adding an arrow ->, followed by the type, after you close the input parameter parenthesis.
from typing import Tuple # This is not necessary in Python 3.9 and later, simply replace Tuple with tuple
def split_name(full_name: str) -> Tuple[str, str]: # Notice the arrow followed by the return type
first, last = full_name.split(maxsplit=1)
return first, last
Here, str means the function expects a text string, and tuple[str, str] means it will return a pair of strings.
You can combine both approaches for maximum clarity:
from typing import List, Optional
def find_student(
student_id: int,
class_list: List[str],
case_sensitive: bool = True
) -> Optional[str]:
"""Search for a student in the class list.
Args:
student_id: The ID number of the student
class_list: List of student names to search through
case_sensitive: Whether to perform case-sensitive search
Returns:
The student's name if found, None otherwise
Raises:
ValueError: If student_id is negative
"""
if student_id < 0:
raise ValueError("Student ID cannot be negative")
# Function implementation here
pass
When documenting functions, try to include:
A clear description of what the function does
All parameters and what kind of data they should be
What the function returns
Any errors that might occur
Examples of how to use the function if it’s complex
Any important notes or warnings
7.11. Summary#
Functions are reusable code blocks that perform specific tasks, aiding code organization and readability.
Functions have local and global variable scopes, impacting variable visibility and modification.
Functions are defined using the def keyword and called by their name.
Functions can accept zero or more arguments (parameters) that allow you to pass data into the function.
You can choose which specified values or variables within the function to be outputted using the return keyword.
Lambda functions are small, one-line functions defined using the lambda keyword. They are typically used for simple operations or transformations and are often passed as arguments to other functions.
Recursion is a technique where a function calls itself to solve a problem, useful for solving problems that can be broken down into smaller, similar subproblems, such as factorials.