5.
Functions
Functions are a fundamental ingredient in every programming language.
A function
is a special sub-program (details below);
contains code that needs to be written and tested just once, but can be executed multiple times by calling the function;
can receive input and can return output.
Using functions makes a program easier to maintain and understand.
5.1 Terminology (sub-program, function, procedure)
A routine or sub-program is the generic term for
function (does return a value) and
procedure (does not return a value).
Nevertheless, in the following we will always speak of functions, even if some of them are actually procedures.
5.2 Function syntax
5.2.1 Function definition and usage
The keyword for defining a function is def , followed by the function name and its input parameters in parentheses () .
As for the if statement, there must be a colon : at the end, and the function body must be indented:
def function_name(input_parameters):
[function body]
Input parameters (if any) are separated by commas.
Return parameters, if any, are indicated by the return keyword at the end of the function body. They also get separated by commas:
# Example: function definition.
def rectangle_quantities(width, height):
area = width * height
circumference = 2 * (width + height)
diagonal = (width**2 + height**2)**0.5
return (area, circumference, diagonal) # Parentheses are optional.
Note that in case of multiple return variables, a tuple (with or without parentheses) is recommended.
Exercise 5.1:
Is the "function" from above a function, procedure, routine or a sub-program?
Classify this one:
def give_me_5():
return 5
Once a function is defined, it can be called everywhere in the subsequent program:
# Example: function calls.
w, h = 4.5, 2.8
a, c, d = rectangle_quantities(w, h)
print(f'Area: {a}, circumference: {c}, diagonal: {d:.2f}.')
# Alternative function call by tuple variable as left-hand side.
acd = rectangle_quantities(w, h)
print(f'Output type: {type(acd)}, area: {acd[0]},')
print(f' circumference: {acd[1]}, diagonal: {acd[2]:.2f}.')
Area: 12.6, circumference: 14.6, diagonal: 5.30.
Output type: <class 'tuple'>, area: 12.6,
circumference: 14.6, diagonal: 5.30.
# Example: function definition & call.
def ball_surface_and_volume(radius):
import math
surface = 4 * math.pi * radius**2
volume = 4/3* math.pi * radius**3
return (surface, volume)
r = 1.0
(s, v) = ball_surface_and_volume(r)
print(f'A ball of radius {r} has surface {s:.3f} and volume {v:.3f}.')
A ball of radius 1.0 has surface 12.566 and volume 4.189.
Exercise 5.2:
Write a function which takes given name and family name as input and returns the lengths of both strings.
Call the function with your own name as input and print the results.
# Solution to Exercise 5.2.
def count_letters(given_name, family_name):
num_given = len(given_name)
num_family = len(family_name)
return num_given, num_family
n1, n2 = count_letters('Helmut', 'Kohl')
print(f'The name has {n1} and {n2} letters, respectively.')
The name has 6 and 4 letters, respectively.
5.2.2 Terminology (parameters vs. arguments)
In the function definition, in- and output are called parameters.
In contrast, when calling a function, one speaks of in-/output arguments.
5.2.3 Keywords arguments
Usually, input arguments are identified by their position inside the parentheses ("order matters").
Alternatively, the input parameter names can be used when calling a function in the form argument_name=value :
# Example: keyword arguments.
def family_reunion(father, mother, child):
print(f'{child} is {father}\'s and {mother}\'s child.')
family_reunion('Sven', 'Anna', 'Jürgen') # Works. Default format.
family_reunion(mother='Anna', child='Jürgen', father='Sven') # Works. Keyword format with switched order.
family_reunion('Sven', child='Jürgen', mother='Anna') # Works. Mixture of positional and keyword format.
#family_reunion('Sven', child='Jürgen', 'Anna') # Error. After the first keyword, no position format al
Jürgen is Sven's and Anna's child.
Jürgen is Sven's and Anna's child.
Jürgen is Sven's and Anna's child.
Exemplarily mother='Anna' is called keyword argument and helps to clarify the role of input arguments.
It also allows to change the order of input arguments (cf. line 7 above).
5.2.4 Default parameter values
A similarly looking, but different concept are default parameter values. One can assign a default value to an input parameter in the form
parameter_name=default_value .
This makes the input argument (i.e., when calling the function) optional, such that it can be omitted.
In such a case, the default value is used instead.
# Example: default parameter values.
def glass_price(width, height, thickness=0.004, price_per_m3=20000):
# All measures in meters. Price in Euros.
volume = width * height * thickness
price = volume * price_per_m3
return price
w, h, d = 0.60, 1.10, 0.005
unit_price = 18000
# Regular function call.
price1 = glass_price(w, h, d, unit_price)
print(f'Window price for non-default setting: {price1:.2f} Euros.')
# Making use of optional arguments.
price2 = glass_price(w, h)
print(f'Window price for default setting: {price2:.2f} Euros.')
# Making use of first optional argument:
# combination of keyword argument and default parameter (!).
price3 = glass_price(w, h, price_per_m3=unit_price)
print(f'Window price for semi-default setting: {price3:.2f} Euros.')
Window price for non-default setting: 59.40 Euros.
Window price for default setting: 52.80 Euros.
Window price for semi-default setting: 47.52 Euros.
Exercise 5.3: Write a working program with a function tip_calculator having input parameters food_price , tip_rate ,
minimum_tip , but no input arguments.
# Solution to Exercise 5.3.
def tip_calculator(food_price=0.0, tip_rate=0.10, minimum_tip=2.0):
tip = food_price * tip_rate
if tip < minimum_tip:
tip = minimum_tip
return tip
t = tip_calculator()
print(f'Give the waiter {t} Euros!')
Give the waiter 2.0 Euros!
5.3 Miscellaneous about in- and output
A function can receive any input type and return any output type, for instance str , bool , dictionaries etc.:
# Example: various input and output types.
def simple_dictionary(key, value):
simple_dict = {}
simple_dict[key] = value
return simple_dict
k, v = 'OEM', 'Audi'
first_dict = simple_dictionary(k, v)
print(first_dict)
k = 100
second_dict = simple_dictionary(k, v)
print(second_dict)
{'OEM': 'Audi'}
{100: 'Audi'}
A function does not need to have any input or output variables:
# Example: no input parameters, no output parameters.
def empty_line():
print('\n')
print('Some results.')
empty_line()
print('Some more results.')
Some results.
Some more results.
If a procedure gets a return argument assigned, it is implicitly set to None .
This Python keyword means "does not exist" or "no value at all".
(It also appears in other contexts, not only for non-existing return parameters.)
def print_something(something):
print(something)
b = print_something(12)
print(f'Return argument to "print_something()" is {b}.')
12
Return argument to "print_something()" is None.
Conversely: even if the function returns some output, it is possible to not assign any return argument.
Sometimes, certain return arguments are not of interest for a specific function call.
Here, the underscore operator _ acting as a placeholder helps to fulfill the syntactical requirements but skip the unnecessary
output:
# Example: underscore operator.
#a, c = rectangle_quantities(w, h) # Syntax error, since the function returns three output variables.
w , h = 3.0, 5.2
a, c, _ = rectangle_quantities(w, h)
print(a, c)
15.600000000000001 16.4
Exercise 5.4: Use the underscore operator to only take care of your family name's length (see Exercise 5.2).
# Solution to Exercise 5.4.
_, n2 = count_letters('Helmut', 'Kohl')
print(f'The family name has {n2} letters.')
The family name has 4 letters.
5.4 Guidelines for good programming style
Use at most one return statement per function.
Restrict a function to a maximum size of 100 code lines. (Some people say, at most 20!)
For a larger function, one should think how it can be broken into smaller pieces, which then can be realized by subfunctions.
Important: Choose simple, yet intuitive function names that describe what the function does ("what", not "how").
The naming of the function parameters should follow the same principle.
Exercise 5.5: Review your functions from exercises in Chapter 5. What can be improved looking at the function and parameter names?
5.5 Important built-in functions
We have already encountered some of Python's built-in functions: type() , int() , float() , bool() , len() .
Other useful built-in function include:
max(multiple comma-separated arguments) , min(multiple comma-sep. args.)
abs(number)
sum(list/tuple)
round(number, [number_significant_digits])
help(function name)
# Example: usage of built-in functions.
x, y, z = 12, -3.4, 1101
print(f'Maximum of {x}, {y}, and {z}: {max(x, y, z)}')
# This even works for alphabetic ordering:
s1, s2 = 'abc', 'abd'
print(f'Maximum of {s1} and {s2}: {max(s1, s2)}.')
a = -5
print(f'a: {a}, |a|: {abs(a)}.')
time_increments = [0.5, 0.6, 0.45, 0.7, 0.55]
print(f'Total time elapsed: {sum(time_increments)} s.')
r = 2.778
print(f'Rounding {r} to nearest integer: {round(r)}; \
to two significant digits: {round(r, 2)}.')
Maximum of 12, -3.4, and 1101: 1101
Maximum of abc and abd: abd.
a: -5, |a|: 5.
Total time elapsed: 2.8 s.
Rounding 2.778 to nearest integer: 3; to two significant digits: 2.78.
Exercise 5.6:
Inform yourself about the built-in function round() .
Use the function to round pi = 3.14159265 to 3 digits after the floating point without a f-string.
# Solution to Exercise 5.6
help(round)
import math
my_pi = round(math.pi, 3)
print(my_pi)
Help on built-in function round in module builtins:
round(number, ndigits=None)
Round a number to a given precision in decimal digits.
The return value is an integer if ndigits is omitted or None. Otherwise
the return value has the same type as the number. ndigits may be negative.
3.142
Tip: One can write help explanations for own functions by adding ''' My explanations ... ''' after the function headline.
Try it out (or see the example below)!
Homework 5.1: The data for a company's annual report are stored in the file annual_performance.json (see the e-learning site for
download).
1. View this file in a text editor to see its dictionary structure. Then write a program that opens the file and extracts the expenses as well
as the sales.
2. Create a function which takes two lists as input parameters. It should compute the overall expenses, overall sales and associated
balance of accounts. Call the function in your program and print the key indicators.
3. Add a block comment to your function (short description what is does) and inform yourself about it.
# Solution to Homework 5.1.
import json
def key_indicators(expenses, sales):
''' Computes overall expenses/sales as well as the associated balance of accounts. '''
overall_expenses = sum(expenses)
overall_sales = sum(sales)
balance = overall_sales - overall_expenses
return (balance, overall_sales, overall_expenses)
file_path = './annual_performance.json'
with open(file_path) as file:
company_data = json.load(file)
expenses = list(company_data["expenses"].values())
sales = list(company_data["sales"].values())
# The main function call:
(balance, overall_sales, overall_expenses) = key_indicators(expenses, sales)
print(f'Annual report:\nThe company has a balance of {balance:.2f} Euros.')
print(f'It sold goods worth {overall_sales:.2f} Euros.')
print(f'It had overall expenses of {overall_expenses:.2f} Euros.')
Annual report:
The company has a balance of -153890.57 Euros.
It sold goods worth 8872016.19 Euros.
It had overall expenses of 9025906.76 Euros.
5.6 Recursive functions
A function can call other functions, including itself. Let us have a closer look.
Some use cases, for example searching through folders and their subfolders, require recursion.
Recursion is the reduction of a problem to a simpler/smaller/previous version of that problem.
A recursive function realizes this concept by repeatedly calling itself.
A classical example is the computation of the unary operator factorial. For instance, 5 ! := 5 ⋅ 4 ⋅ 3 ⋅ 2 ⋅ 1 which is 120.
By definition, 0 ! := 1 and 1 ! := 1.
Observation: n ! = n ⋅ (n − 1) ⋅ (n − 2) ⋅ … ⋅ 1 = n ⋅ (n − 1) !
# Example: recursive function.
def input_check_nonnegative_integer(n):
if type(n) != int or n < 0:
raise ValueError(f'Invalid input: nonnegative integer expected, but {n} received.')
def factorial(n):
''' Compute the expression n! recursively by using the relation
n! = n x (n-1)!.
Author: Kurt Gödel.
'''
input_check_nonnegative_integer(n)
factorial_n = 1
if n > 1:
factorial_n = n * factorial(n-1) # The function calls itself.
return factorial_n
print(f'5! = {factorial(5)}, 12! = {factorial(12)}, 0! = {factorial(0)}.')
5! = 120, 12! = 479001600, 0! = 1.
help(factorial)
Help on function factorial in module __main__:
factorial(n)
Compute the expression n! recursively by using the relation
n! = n x (n-1)!.
Author: Kurt Gödel.
More about raise , errors and exceptions later.
Careful: When implemented incorrectly, a recursive function can easily end up in a state where the program would not terminate:
# Example: buggy recursive function not terminating.
def buggy_factorial(n):
factorial_n = n * buggy_factorial(n-1)
return factorial_n
print(f'5! = {buggy_factorial(5)}.')
----------------------------------------------------------------
RecursionError Traceback (most recent call last)
Cell In[18], line 8
4 factorial_n = n * buggy_factorial(n-1)
6 return factorial_n
----> 8 print(f'5! = {buggy_factorial(5)}.')
Cell In[18], line 4, in buggy_factorial(n)
3 def buggy_factorial(n):
----> 4 factorial_n = n * buggy_factorial(n-1)
6 return factorial_n
Cell In[18], line 4, in buggy_factorial(n)
3 def buggy_factorial(n):
----> 4 factorial_n = n * buggy_factorial(n-1)
6 return factorial_n
[... skipping similar frames: buggy_factorial at line 4 (2974 times)]
Cell In[18], line 4, in buggy_factorial(n)
3 def buggy_factorial(n):
----> 4 factorial_n = n * buggy_factorial(n-1)
6 return factorial_n
RecursionError: maximum recursion depth exceeded
Such infinite loops are prevented through the system by prescribing a maximum recursion depth.
(This does not mean, though, that the buggy code does work then!)
import sys
print(sys.getrecursionlimit())
3000
Keep in mind: It is the responsibility of the programmer to avoid programming errors.
Exercise 5.7: The Fibonacci recursion is a two-term recursion defined by
f n := f n − 1 + f n − 2 for n ≥ 3
and "start ramp" f 1 := 1, f 2 := 1.
The first ten Fibonacci numbers are 1, 1, 2, 3, 5, 8, 13, 21, 34, 55.
Write a program that computes the n-th Fibonacci number f n recursively.
However: Why is it not a good idea to use a recursive approach (going backwards) for Fibonacci numbers?
The concept of iterations in Chapter 6 is a better choice to handle such two-term recursions.
# Solution to Exercise 5.x.
def fibonacci(n):
if n < 3:
fn = 1
else:
fn = fibonacci(n-1) + fibonacci(n-2)
return fn
print(f'f_10 = {fibonacci(10)}')
f_10 = 55
Homework 5.2: Binomial coefficients
Write a program to compute the binomial coefficient ()
n
k
:=
n!
k! (n−k) !
for 0 ≤ k ≤ n .
First write a recursive function prod(i, j) which returns the product i ⋅ (i + 1) ⋅ … ⋅ j.
In particular, n! == prod(1, n) for n >= 1 .
Call this function three times to compute the binomial coefficient. The corresponding integers n and k should be enterd by the
user.
Make sure that the program also handles inputs k == 0 and n == 0 correctly. (There holds 0 ! = 1.)
# Solution to Homework 5.2.
def prod(i, j):
prd = max(1, j) # This makes sure that even for j=0, prd is 1.
if j > i:
prd = prod(i, j-1) * j
return prd
def binomial(n, k):
n_fact = prod(1, n)
k_fact = prod(1, k)
n_k_fact = prod(1, n-k)
bin_coeff = n_fact // (k_fact * n_k_fact)
return bin_coeff
n = int(input('Enter n: '))
k = int(input('Enter n: '))
bin_coeff = binomial(n, k)
print(f'{n} over {k} equal to {bin_coeff}.')
Enter n: 8
Enter n: 3
8 over 3 equal to 56.
5.7 Purpose of functions
As a round-up for Chapter 5, there are several arguments why functions should be used, especially in more extensive programs:
Re-usability: Code needs to be written just once, which saves time.
Maintainability: When the function code needs to be maintained or modified, it is sufficient to do so in just one place.
Understandability: When a block of code is put into a properly named function, its purpose becomes immediately clear, also
shedding on the semantics (meaning) of the code lines.
Readability: By grouping code into several well-defined code blocks, the program structure becomes evident on an abstract level,
comparable to the outline of a textbook.
Testability: When the code consists of functions acting as program units, the testing concept of unit tests can be applied.
This way, the entire program can be tested systematically.
Processing math: 100%