Python: Functions

Functions

Python has a lot of built-in functions (overview) and we have already used some of them. But what is a function?

A function is a block of code with a name (address), thus it is callable and only executed if it is called.
This also means that you can call a (initialized) function everywhere in the scope (see below) of your program, similar to a variable.

Commonly we will insert data into a function. The data is processed inside the function, then the new data is returned or output.

Let’s have a look at the Pure Data example again. Two numbers are added with an operator called +. Now we will write a function which does the same:
It takes two arguments as inputs and returns the sum of them.

pd_addition.gif Small program in the visual programming language Pure Data.

Defining a function

Functions are stored as variables (or – in terms of Python – as objects) as well, so the naming rules and conventions for variables apply for functions as well of course.
The keyword to define a function is def, followed by a name of our choice, which we will then use to call the function.

The name of our function is followed by two `()`. These define the space were we can insert data. With variable names of our choice we can access this data inside the function.
def add_numbers(num1, num2):
    # The variables num1 and num2 are accessible 
    # inside the function.
    print(f'{num1} + {num2} is {num1 + num2}')
We have already learned that each line of code is one independent statement.
Inside a function we can write multiple statements (each in one line), which all belong to the function and are executed when we call the function. To mark code as inside a function, all lines are indent by one tab (commonly 4 spaces). The `:` at the end of the `def`-line indicates that we have to indent the following line(s).

If we execute the block of code, we’ll create a new object with the name add_numbers.

Then we can call our function and insert two values into it. The function will print the result.

add_numbers(3, 5)
'3 + 5 is 8'
left, right = 11, -12
add_numbers(left, right)
'11 + -12 is -1'
Task: Modify the function from above so that it will display just a `-` if the second number is negative. (Output should be `11 - 12 = -1`).
Likely you'll need the function abs() for that. Try abs(-12) or help(abs) to see what it does.
def add_numbers (a, b):
    # if b is 0 or positive:
    if b >= 0:
        print(f'{a} + {b} = {a+b}')
    # if b is negative:
    else:
        print(f'{a} - {abs(b)} = {a+b}')
        # another solution:
        # print(f'{a} - {b * -1} = {a+b}')
        
left, right = 11, -12
add_numbers(left, right)
11 - 12 = -1
The great thing about functions (as they are objects like variables) is that we can call them as often as we want inside our program after we have initiated them.
This can save a lot of time, because we can reuse existing code.

Arguments

It’s possible and often useful (for testing, structuring, reusability) to write a function although we want to use it only once. But the strength appears when we need to execute a block of code on multiple occurences in our program. Let’s imagine we have a row of hearts which we want to print on several pages on several positions. We could write the whole code for each occurence or wrap everything in a function and then call this function when needed.

from fpdf import FPDF

Without arguments

def print_hearts_top():
    heart_01 = '<3' * 50
    pdf.set_font('times', '', 15)
    pdf.set_xy(10, 10)
    pdf.multi_cell(0, 0, txt=heart_01, align='C')

def print_hearts_bottom():
    heart_01 = '<3' * 50
    pdf.set_font('times', '', 15)
    pdf.set_xy(10, 275)
    pdf.multi_cell(0, 0, txt=heart_01, align='C')
pdf = FPDF('P', 'mm', format='A4')
pdf.add_page()
print_hearts_top()
print_hearts_bottom()
pdf.output('hearts_001.pdf')

First this looks like more work but if we need the row of hearts again, we just need to call our function and they will be inserted.

With arguments

One aim when defining functions is to make them flexible. Flexibility is reached through the use of variables. First we can set the position (set_xy) as variables, then we can reduce the two functions to one:

def print_hearts(x, y):
    heart_01 = '<3' * 50
    pdf.set_font('times', '', 15)
    # Next we'll use the arguments x and y which we passed to the function
    pdf.set_xy(x, y)
    pdf.multi_cell(0, 0, txt=heart_01, align='C')
pdf = FPDF('P', 'mm', format='A4')
pdf.add_page()
print_hearts(10, 10)
print_hearts(10, 275)
pdf.output('hearts_002.pdf')

With default arguments

When defining a function we can insert default arguments for the variables (parameters). If no arguments are provided when calling the function, the default arguments will be used:

def print_hearts(y, x=10, num=50):
    pdf.set_font('times', '', 15)
    pdf.set_xy(x, y)
    pdf.multi_cell(0, 0, txt='<3' * num, align='C')

We have to take care of the order of the variables. Variables without a default argument have to be placed before variables with default argument.

pdf = FPDF('P', 'mm', format='A4')
pdf.add_page()
print_hearts(y=10)
print_hearts(y=275)
pdf.output('hearts_003.pdf')

Return

At the beginning was stated that functions return data. This means that they can give back data after it was processed. This is very useful, because so the data can be stored in a variable again.

def simple_return(data):
    return data

d = simple_return('some data')
print(d)
some data
def double_return(data):
    return data*2

d = double_return('is a rose ')
print(d)

d = 'A rose' + double_return('is a rose ')
print(d)
is a rose is a rose 
A roseis a rose is a rose 
def add_numbers(num1, num2):
    res = num1 + num2
    return res

r = add_numbers(14, 5)
r
19

Scope of variables and functions

Variables and functions have a specific scope, in which you can access (and override) them.
Variables initiated in the main program (not indented) are accessible globally = everywhere, even inside functions.
x = 1 # global scope

def print_x():
    print(x)
    
print_x()
1

If we define a variable inside of a function, it is available only inside this function:

x = 1

def print_x():
    x = 56  # This x is a new variable, avaible only inside the function = local.
    print(x)
    
print_x()

print(x) # The outside x is not affected by the operations inside the function.
56
1

We can change the value of a global variable from inside a function, if we address the global variable through the keyword global:

def override_x(new_value):
    global x
    x = new_value
    
override_x(71)
print(x)
71

The same applies to functions. We can write functions inside functions, which are available only to their parent function. But that belongs to a later session about classes.

def outside_function(x, y):
    def inside_function(x, y):
        return x + y
    
    return inside_function(x, y)

outside_function(7,4)
11
inside_function(7,4)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-28-158045f32788> in <module>
----> 1 inside_function(7,4)

NameError: name 'inside_function' is not defined

Recursion

Q: How do I explain recursion to a 4 year-old?

A: Explain it to someone a year younger than you & ask them to do the same.

Recursion is a topic on its own and this is only a brief introduction. Functions can be recursive which means that they can call themselve from inside themselve.

numbers = [x for x in range(4, 11, 3)]
print(numbers)

def add_recursive(numbers):
    if len(numbers) == 1:
        return numbers[0]
    else:
        return numbers[0] + add_recursive(numbers[1:])
    
summed = add_recursive(numbers)
print(summed)
[4, 7, 10]
21
numbers = [x for x in range(4, 11, 3)]
print(numbers)

def add_recursive(numbers):
    if len(numbers) == 1:
        return numbers.pop()
    else:
        numbers[0] += numbers.pop()
        return add_recursive(numbers)
    
summed = add_recursive(numbers)
print(summed)
[4, 7, 10]
21