Chapter 9: Function definitions

Generic badge Open this in Colab

The function definition

The concept of functions is probably one of the most important in mathematics. In programming languages, too, functions are often used to convert mathematical functions into algorithms. Such a function calculates one or more values and is completely dependent on the input values transferred, the so-called arguments. Beyond the mathematical scope, functions can also be use to fulfill a certain functionality or operation. E.g., it can be very useful to define some parts of your code as a function, if you have to use these parts repeatedly and/or are of complex structure. The general structure of a function definition in Python is as follows:

def function_name(argument_list):
    commands(using elements of the argument_list)
    return some_variable
# e.g.:
def my_function(x,y):
    sum=x+y
    return sum
print(my_function(77,23))
>>>
100
  • function_name can be any name you want to give your function. Avoid to use names, which are identical to Python’s built-in function names (list, len, sum, print, …)
  • argument_list is a list of one or more variables, that are passed to the function. E.g., in maths, this concept would correspond to \(y=y(x)\), \(y\) a function of \(x\), while \(x\) is the argument_list. The argument_list can of course contain more than one parameter, similar to, e.g., \(y=y(x, t, r, ...)\).
  • commands are commands, that are executed within and only within the function (encapsulated) and can use the input parameter from the argument_list.
  • whatever has been calculated or processed inside the function can be returned with variables provided after the return command at the end of the function. If you have nothing to return back to your main script, you can skip this point.

Note: The function has to be defined prior to its first call within your script!

Note: Avoid to use identical names for variables in your argument list and for variables in your script. This is not prohibited and Python can handle that, but your code can mess-up very quickly and you quickly lose track of your defined and (somewhere else) processed variables.

Note: As for for-loops and if-conditions, Python relies on indentation (whitespace at the beginning of a line) to define the scope of a code block in your code.

Let’s apply this concept to the last example from the if-conditions chapter:

new_number_list = list(range(0,22,1))
threshold = 10
for number in new_number_list:
    if number>threshold:
        print(f"match! {number} is greater than {threshold}.")
    else:
        print(f"{number} is below the threshold {threshold}!")

We want to encapsulate the operation of checking the threshold-condition within a new function threshold_checker:

# the new function definition:
def threshold_checker(value, threshold):
    if value>threshold:
        print(f"match! {value} is greater than {threshold}.")
    else:
        print(f"{value} is below the threshold {threshold}!")

new_number_list = list(range(0,22,1))
threshold_to_check = 10

for number in new_number_list:
    threshold_checker(number, threshold_to_check)

0 is below the threshold 10!
1 is below the threshold 10!
2 is below the threshold 10!
3 is below the threshold 10!
4 is below the threshold 10!
5 is below the threshold 10!
6 is below the threshold 10!
7 is below the threshold 10!
8 is below the threshold 10!
9 is below the threshold 10!
10 is below the threshold 10!
match! 11 is greater than 10.
match! 12 is greater than 10.
match! 13 is greater than 10.
match! 14 is greater than 10.
match! 15 is greater than 10.
match! 16 is greater than 10.
match! 17 is greater than 10.
match! 18 is greater than 10.
match! 19 is greater than 10.
match! 20 is greater than 10.
match! 21 is greater than 10.

# re-run with a different threshold:
threshold_to_check = 15
for number in new_number_list:
    threshold_checker(number, threshold_to_check)

0 is below the threshold 15!
1 is below the threshold 15!
2 is below the threshold 15!
3 is below the threshold 15!
4 is below the threshold 15!
5 is below the threshold 15!
6 is below the threshold 15!
7 is below the threshold 15!
8 is below the threshold 15!
9 is below the threshold 15!
10 is below the threshold 15!
11 is below the threshold 15!
12 is below the threshold 15!
13 is below the threshold 15!
14 is below the threshold 15!
15 is below the threshold 15!
match! 16 is greater than 15.
match! 17 is greater than 15.
match! 18 is greater than 15.
match! 19 is greater than 15.
match! 20 is greater than 15.
match! 21 is greater than 15.

# re-run with a different number list:
new_number_list_2 = list(range(10,35,1))
threshold_to_check = 15
for number in new_number_list_2:
    threshold_checker(number, threshold_to_check)

Exercise 1

  1. Re-write the threshold_check function, so that it also contains and executes the for-loop. The argument value is then a list of numbers (the new_number_list) and no longer a single value.
# Your solution here:

Toggle solution
# Solution:
# the new function definition:
def threshold_checker_for_lists(inputlist, threshold):
    for value in inputlist:
        if value>threshold:
            print(f"match! {value} is greater than {threshold}.")
        else:
            print(f"{value} is below the threshold {threshold}!")

new_number_list  = list(range(0,22,1))
new_number_list_2= list(range(10,35,1))
new_number_list_3= list(range(35,40,1))
threshold_to_check = 10

threshold_checker_for_lists(new_number_list, threshold_to_check)


# you can also plug in numbers directly:
threshold_checker_for_lists(new_number_list, 18)
# but has disadvantages (e.g. when you want to change the
# threshold )
# what "disadvantage" means:
#threshold_checker_for_lists(new_number_list, 30)
#threshold_checker_for_lists(new_number_list_2, 30)
#threshold_checker_for_lists(new_number_list_3, 31)

#better:
threshold_to_check=30
threshold_checker_for_lists(new_number_list, threshold_to_check)
print("")
threshold_checker_for_lists(new_number_list_2, threshold_to_check)
print("")
threshold_checker_for_lists(new_number_list_3, threshold_to_check)
# another way to call your function:
#inputlist, threshold
threshold_checker_for_lists(inputlist=new_number_list,
                            threshold=threshold_to_check)

Expand your new function, so that it

  1. remembers the first occurrence of a positive threshold-check, and
  2. returns that value to your main script, that prints it out there.
  3. To validate your function, change the range of your input list to new_number_list = list(range(3,22,1)).

0: 9 is below the threshold 10!
1: 10 is below the threshold 10!
2: match! 11 is greater than 10.
3: match! 12 is greater than 10.
4: match! 13 is greater than 10.
5: match! 14 is greater than 10.
6: match! 15 is greater than 10.
7: match! 16 is greater than 10.
8: match! 17 is greater than 10.
9: match! 18 is greater than 10.
10: match! 19 is greater than 10.
11: match! 20 is greater than 10.
12: match! 21 is greater than 10.
first threshold-check at index 2

Toggle solution
# Solution:
# the modified function definition:
def threshold_checker_for_lists(inputlist, threshold):
    first_occurrence = -1 # -1 acts as a place holder/initial
                         #    value
    for index in range(len(inputlist)):
        if inputlist[index]>threshold:
            print(f"{index}: match! {inputlist[index]} is "
                  f"greater than {threshold}.")
            if first_occurrence==-1:
                first_occurrence=index
        else:
            print(f"{index}: {inputlist[index]} is below the "
                  f"threshold {threshold}!")
    return first_occurrence

new_number_list = list(range(9,22,1))
threshold_to_check = 10

result = threshold_checker_for_lists(new_number_list, \
                                     threshold_to_check)

print(f"first threshold-check at index {result}")

Exercise 2

Write a function, that converts temperatures \(T\) from Fahrenheit ([F]) to Celsius ([°C]). The formula for the conversion is:

\[T[^\circ\text{C}] = \frac{5}{9} * (T[\text{F}]-32)\]
# Your solution here:

26.0
78.80000000000001

Toggle solution
# Solution:
def fahrenheit_to_celsius(temp_in_fahrenheit):
    """
        This function converts an input temperature in
        Fahrenheit to Celsius.

        input:
            temp_in_fahrenheit - input temperature [F]
        output:
            temp_in_celsius    - temperature [°C]

    """
    temp_in_celsius = 5/9 * (temp_in_fahrenheit- 32)
    return temp_in_celsius


def celsius_to_fahrenheit(temp_in_celsius):
    """
        This function converts an input temperature in Celsius
        to Fahrenheit

        input:
            temp_in_celsius     - input temperature [°C]
        output:
            temp_in_fahrenheit  - temperature [F]

    """
    temp_in_fahrenheit = 9/5 * (temp_in_celsius) + 32
    return temp_in_fahrenheit

print(fahrenheit_to_celsius(78.8))
print(celsius_to_fahrenheit(26))

Into: Note the discrepancy of the two results. One reason is the internal precision of the defined/used variable types, due to which rounding errors can occur. Later we will learn, how to change the precision of a variable type with NumPy.

Exercise 3

Write a function product, that multiplies all numbers of a given number-list. Apply your function to the list list1=[4, 8, 12, 16, 20, 32, 28, 24, -1].

2642411520

# Your solution here:

Toggle solution
# Solution:
def product(inputlist):
    """Calculates the product of a given number-list"""
    result = 1 # again, we need an initialization of the later result variable
    for index in range(0,len(inputlist)):
        result = result * inputlist[index]
    return result

list1=[4, 8, 12, 16, 20, 32, 28, -24, -1]
print(product(list1))

Exercise 4

Write a function, that finds the maximum value of a given list. Apply your function to the list defined in Exercise 2. Extra: Also print out the index of the maximum value.

# Your solution here:

maximum: 40, at index: 4

Toggle solution
# Solution:
def max(inputlist):
    index_of_max = 0
    maximum = inputlist[index_of_max]
    # maximum = inputlist[0]
    #index_of_max = 0
    for index in range(1,len(inputlist)):
        if inputlist[index] > maximum:
            maximum = inputlist[index]
            index_of_max = index
    return maximum, index_of_max

list1=[4, 8, 12, 16, 40, 32, 28, -24, -1]
current_max, current_max_index = max(list1) # one possible
                                            #  output format
#(current_max, current_max_index) =  max(list1) # also okay
print(f"maximum: {current_max}, at index: {current_max_index} ")


The *args variable in function definitions

There are very often cases in which the number of arguments required for the call of a function is not known a priori. E.g.:

#Example 1:
def test_static_args(arg1, arg2, arg3):
    print("arg1:", arg1)
    print("arg2:", arg2)
    print("arg3:", arg3)
print(test_static_args(1, 'word', 'word 2'))

arg1: 1
arg2: word
arg3: word 2
None

Works as defined. But what if you need to add another argument or less then two?

# e.g.:
print(test_static_args(1, 'word'))

TypeError...

To overcome this problem we can use a so-called tuple reference, which is preceded by * and put at the end of the argument list:

#Example 2:
def test_var_args(f_arg, *args):
    print("first normal arg:", f_arg)
    for counter, arg in enumerate(args, 1):
        print(f"{counter}. optional arg: {arg}")
    return 'end.'

print(test_var_args(1, 'i am optional 1', 'optional 2'))

first normal arg: 1 1. optional arg: i am optional 1 2. optional arg: optional 2 end.

print(test_var_args(1, 'only 1 optional'))

first normal arg: 1 1. optional arg: only 1 optional end.

print(test_var_args(1))

first normal arg: 1 end.

print(test_var_args('fixed', '1', '2', '3', '4'))

first normal arg: fixed 1. optional arg: 1 2. optional arg: 2 3. optional arg: 3 4. optional arg: 4 end.

You can even skip the standard argument (which is otherwise always required) and work just with *args:

#Example 3:
def test_var_args2(*args):
    for counter, arg in enumerate(args, 1):
        print(f"{counter}. optional arg: {arg}")
    return 'end.'

print(test_var_args2(1, 'word', 5))

1. optional arg: 1 2. optional arg: word 3. optional arg: 5 end.

print(test_var_args2(1, 'word'))

4. optional arg: 1 5. optional arg: word end.

print(test_var_args2())

end.

With *args, we are no longer passing a list to the function, but several different positional arguments. The function takes all the arguments that are provided in the input and packs them all into a single iterable object named args. args is just a name, you can choose any other name that you prefer.

The *kwargs variable in function definitions

**kwargs works just like *args, but instead of accepting positional arguments it accepts keyword (named) arguments (like dictionaries). E.g.:

#Example 1:
def my_concatenate1(**kwargs):
    result = ""
    # Iterating over the keys' values of the kwargs dictionary:
    for value in kwargs.values():
        result += value
    return result

print(my_concatenate1(word1="Somewhere ", word2="over ", word3="the ", word4="rainbow."))

Somewhere over the rainbow.

and

#Example 2:
def my_concatenate2(**kwargs):
    result = ""
    # Iterating over the keys of the kwargs dictionary:
    for key in kwargs:
        result += key
    return result

print(my_concatenate2(word1="Somewhere ", word2="over ", word3="the ", word4="rainbow."))

word1word2word3word4

or combine keyword with its corresponding value:

#Example 3:
def my_kwargs_printer(**kwargs):
    result = ""
    # Iterating over the keys and values of the kwargs dictionary:
    for key, value in kwargs.items():
        print(f"the key {key} has the value {value}.")
    return 'done.'

print(my_kwargs_printer(word1="Somewhere ", word2="over ", word3="the ", word4="rainbow."))

the key word1 has the value Somewhere .
the key word2 has the value over .
the key word3 has the value the .
the key word4 has the value rainbow..
done.

Also here, kwargs is just a name that can be changed to whatever you want to.

And of course you can combine standard arguments with *args and *kwargs, just keep the following order:

  1. standard arguments
  2. *args arguments
  3. **kwargs arguments

Predefined optional function arguments

Another very useful and handy way, to define optional arguments of a function are predefined, but still optional arguments. By assigning an initial value to an argument in the argument list converts that specific argument automatically to an optional, but predefined argument:

# Example:
def my_func(x, y=5):
    print(f"x:{x}, y:{y}")

Here, x is a standard argument that must be provided in any call of the function. y=5 is the optional, predefined argument with the initial value 7. You can call the function in different, equivalent ways:

my_func(7, 8) # means, x=7, y=8

x:7, y:8

You can even write the argument’s name within the function call, so that the assignment of each value to a specific argument becomes even more clear:

my_func(7, y=8)
my_func(x=7, y=8)
my_func(y=8, x=7) # by "naming" each argument,
                  # you can even re-order the input arguments
my_func(x=19)

x:7, y:8
x:7, y:8
x:7, y:8
x:19, y:5

Leaving the optional parameter y unassigned will cause the function to use its predefined value:

my_func(7)

x:7, y:5

But leaving the standard argument (x) will cause an error:

my_func()

TypeError...

With predefined, but still optional arguments you are more flexible in the way you use your optionally assigned arguments (e.g. when you prototype your function or if you want to keep your main routine flexible and modular).

updated: