Functions

Syntax

Functions

The concept of a function is one of the most important in mathematics. A common usage of functions in computer languages is to implement mathematical functions. Such a function is computing one or more results, which are entirely determined by the parameters passed to it.

This is mathematics, but we are talking about programming and Python. So what is a function in programming? In the most general sense, a function is a structuring element in programming languages to group a bunch of statements so they can be utilized in a program more than once. The only way to accomplish this without functions would be to reuse code by copying it and adapting it to different contexts, which would be a bad idea. Redundant code - repeating code in this case - should be avoided! Using functions usually enhances the comprehensibility and quality of a program. It also lowers the cost for development and maintenance of the software.

Functions are known under various names in programming languages, e.g. as subroutines, routines, procedures, methods, or subprograms.

A function in Python is defined by a def statement. The general syntax looks like this:

def function-name(Parameter list):
    statements, i.e. the function body

The parameter list consists of none or more parameters. Parameters are called arguments, if the function is called. The function body consists of indented statements. The function body gets executed every time the function is called. We demonstrate this in the following picture:

Function Call: Control Flow

The code from the picture can be seen in the following:

def f(x, y):
    z = 2 * (x + y)
    return z


print("Program starts!")
a = 3
res1 = f(a, 2+a)
print("Result of function call:", res1)
a = 4
b = 7
res2 = f(a, b)
print("Result of function call:", res2)
Program starts!
Result of function call: 16
Result of function call: 22

We call the function twice in the program. The function has two parameters, which are called x and y. This means that the function f is expecting two values, or I should say "two objects". Firstly, we call this function with f(a, 2+a). This means that a goes to x and the result of 2+a (5) 'goes to' the variable y. The mechanism for assigning arguments to parameters is called argument passing. When we reach the return statement, the object referenced by z will be return, which means that it will be assigned to the variable res1. After leaving the function f, the variable z and the parameters x and y will be deleted automatically.

Function Call: Argument Passing Part1

The references to the objects can be seen in the next diagram:

Function Call: Argument Passing Part2

The next Python code block contains an example of a function without a return statement. We use the pass statement inside of this function. pass is a null operation. This means that when it is executed, nothing happens. It is useful as a placeholder in situations when a statement is required syntactically, but no code needs to be executed:

def doNothing():
    pass

A more useful function:

def fahrenheit(T_in_celsius):
    """ returns the temperature in degrees Fahrenheit """
    return (T_in_celsius * 9 / 5) + 32

for t in (22.6, 25.8, 27.3, 29.8):
    print(t, ": ", fahrenheit(t))
22.6 :  72.68
25.8 :  78.44
27.3 :  81.14
29.8 :  85.64

Default arguments in Python

When we define a Python function, we can set a default value to a parameter. If the function is called without the argument, this default value will be assigned to the parameter. This makes a parameter optional. To say it in other words: Default parameters are parameters, which don't have to be given, if the function is called. In this case, the default values are used.

We will demonstrate the operating principle of default parameters with a simple example. The following function hello, - which isn't very useful, - greets a person. If no name is given, it will greet everybody:

def hello(name="everybody"):
    """ Greets a person """
    print("Hello " + name + "!")

hello("Peter")
hello()
Hello Peter!
Hello everybody!

The Defaults Pitfall

In the previous section we learned about default parameters. Default parameters are quite simple, but quite often programmers new to Python encounter a horrible and completely unexpected surprise. This surprise arises from the way Python treats the default arguments and the effects steming from mutable objects.

Mutable objects are those which can be changed after creation. In Python, dictionaries are examples of mutable objects. Passing mutable lists or dictionaries as default arguments to a function can have unforeseen effects. Programmer who use lists or dictionaries as default arguments to a function, expect the program to create a new list or dictionary every time that the function is called. However, this is not what actually happens. Default values will not be created when a function is called. Default values are created exactly once, when the function is defined, i.e. at compile-time.

Let us look at the following Python function "spammer" which is capable of creating a "bag" full of spam:

def spammer(bag=[]):
    bag.append("spam")
    return bag

Calling this function once without an argument, returns the expected result:

spammer()
Output::
['spam']

The surprise shows when we call the function again without an argument:

spammer()
Output::
['spam', 'spam']

Most programmers will have expected the same result as in the first call, i.e. ['spam']

To understand what is going on, you have to know what happens when the function is defined. The compiler creates an attribute __defaults__:

def spammer(bag=[]):
    bag.append("spam")
    return bag

spammer.__defaults__
Output::
([],)

Whenever we will call the function, the parameter bag will be assigned to the list object referenced by spammer.__defaults__[0]:

for i in range(5):
    print(spammer())
    
print("spammer.__defaults__", spammer.__defaults__)
['spam']
['spam', 'spam']
['spam', 'spam', 'spam']
['spam', 'spam', 'spam', 'spam']
['spam', 'spam', 'spam', 'spam', 'spam']
spammer.__defaults__ (['spam', 'spam', 'spam', 'spam', 'spam'],)

Now, you know and understand what is going on, but you may ask yourself how to overcome this problem. The solution consists in using the immutable value None as the default. This way, the function can set bag dynamically (at run-time) to an empty list:

def spammer(bag=None):
    if bag is None:
        bag = []
    bag.append("spam")
    return bag

for i in range(5):
    print(spammer())
    
print("spammer.__defaults__", spammer.__defaults__)
['spam']
['spam']
['spam']
['spam']
['spam']
spammer.__defaults__ (None,)

Docstring

The first statement in the body of a function is usually a string statement called a Docstring, which can be accessed with the function_name.__doc__. For example:

def hello(name="everybody"):
    """ Greets a person """
    print("Hello " + name + "!")

print("The docstring of the function hello: " + hello.__doc__)
The docstring of the function hello:  Greets a person 

Keyword Parameters

Using keyword parameters is an alternative way to make function calls. The definition of the function doesn't change. An example:

def sumsub(a, b, c=0, d=0):
    return a - b + c - d

print(sumsub(12, 4))
print(sumsub(42, 15, d=10))
8
17

Keyword parameters can only be those, which are not used as positional arguments. We can see the benefit in the example. If we hadn't had keyword parameters, the second call to function would have needed all four arguments, even though the c argument needs just the default value:

print(sumsub(42,15,0,10))
17

Return Values

In our previous examples, we used a return statement in the function sumsub but not in Hello. So, we can see that it is not mandatory to have a return statement. But what will be returned, if we don't explicitly give a return statement. Let's see:

def no_return(x, y):
    c = x + y

res = no_return(4, 5)
print(res)
None

If we start this little script, None will be printed, i.e. the special value None will be returned by a return-less function. None will also be returned, if we have just a return in a function without an expression:

def empty_return(x, y):
    c = x + y
    return

res = empty_return(4, 5)
print(res)
None

Otherwise the value of the expression following return will be returned. In the next example 9 will be printed:

def return_sum(x, y):
    c = x + y
    return c

res = return_sum(4, 5)
print(res)
9

Let's summarize this behavior: Function bodies can contain one or more return statements. They can be situated anywhere in the function body. A return statement ends the execution of the function call and "returns" the result, i.e. the value of the expression following the return keyword, to the caller. If the return statement is without an expression, the special value None is returned. If there is no return statement in the function code, the function ends, when the control flow reaches the end of the function body and the value None will be returned.

Returning Multiple Values

A function can return exactly one value, or we should better say one object. An object can be a numerical value, like an integer or a float. But it can also be e.g. a list or a dictionary. So, if we have to return, for example, 3 integer values, we can return a list or a tuple with these three integer values. That is, we can indirectly return multiple values. The following example, which is calculating the Fibonacci boundary for a positive number, returns a 2-tuple. The first element is the Largest Fibonacci Number smaller than x and the second component is the Smallest Fibonacci Number larger than x. The return value is immediately stored via unpacking into the variables lub and sup:

def fib_intervall(x):
    """ returns the largest fibonacci
    number smaller than x and the lowest
    fibonacci number higher than x"""
    if x < 0:
        return -1
    (old,new) = (0,1)
    while True:
        if new < x:
            (old,new) = (new,old+new)
        else:
            if new == x: 
                new = old+new
            return (old, new)
            
while True:
    x = int(input("Your number: "))
    if x <= 0:
        break
    (lub, sup) = fib_intervall(x)
    print("Largest Fibonacci Number smaller than x: " + str(lub))
    print("Smallest Fibonacci Number larger than x: " + str(sup))
Your number: 5
Largest Fibonacci Number smaller than x: 3
Smallest Fibonacci Number larger than x: 8
Your number: 4
Largest Fibonacci Number smaller than x: 3
Smallest Fibonacci Number larger than x: 5
Your number: 9
Largest Fibonacci Number smaller than x: 8
Smallest Fibonacci Number larger than x: 13
Your number: -1

Local and Global Variables in Functions

Variable names are by default local to the function, in which they get defined.

def f(): 
    print(s)
s = "Python"
f()
Python
def f(): 
    s = "Perl"
    print(s)
f()
Perl
s = "Python"
f()
print(s)
Perl
Python
def f(): 
    print(s)
    s = "Perl"
    print(s)


s = "Python" 
f()
print(s)
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-59-81b2fbbc4d42> in <module>
      6 
      7 s = "Python"
----> 8 f()
      9 print(s)

<ipython-input-59-81b2fbbc4d42> in f()
      1 def f():
----> 2     print(s)
      3     s = "Perl"
      4     print(s)
      5 

UnboundLocalError: local variable 's' referenced before assignment
s = "Python" 
f()
print(s)
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-60-6661890debe5> in <module>
      1 s = "Python"
----> 2 f()
      3 print(s)

<ipython-input-59-81b2fbbc4d42> in f()
      1 def f():
----> 2     print(s)
      3     s = "Perl"
      4     print(s)
      5 

UnboundLocalError: local variable 's' referenced before assignment

If we execute the previous script, we get the error message: UnboundLocalError: local variable 's' referenced before assignment.

The variable s is ambigious in f(), i.e. in the first print in f() the global s could be used with the value "Python". After this we define a local variable s with the assignment s = "Perl".

def f():
    global s
    print(s)
    s = "dog"
    print(s) 
s = "cat" 
f()
print(s)
cat
dog
dog

We made the variable s global inside of the script. Therefore anything we do to s inside of the function body of f is done to the global variable s outside of f.

Arbitrary Number of Parameters

There are many situations in programming, in which the exact number of necessary parameters cannot be determined a-priori. An arbitrary parameter number can be accomplished in Python with so-called tuple references. An asterisk "*" is used in front of the last parameter name to denote it as a tuple reference. This asterisk shouldn't be mistaken for the C syntax, where this notation is connected with pointers. Example:

def arithmetic_mean(first, *values):
    """ This function calculates the arithmetic mean of a non-empty
        arbitrary number of numerical values """

    return (first + sum(values)) / (1 + len(values))

print(arithmetic_mean(45,32,89,78))
print(arithmetic_mean(8989.8,78787.78,3453,78778.73))
print(arithmetic_mean(45,32))
print(arithmetic_mean(45))
61.0
42502.3275
38.5
45.0

This is great, but we have still have one problem. You may have a list of numerical values. Like, for example,

 x = [3, 5, 9] 

You cannot call it with

 arithmetic_mean(x)

because "arithmetic_mean" can't cope with a list. Calling it with

 arithmetic_mean(x[0], x[1], x[2])

is cumbersome and above all impossible inside of a program, because list can be of arbitrary length.

The solution is easy. We add a star in front of the x, when we call the function.

 arithmetic_mean(*x)

This will "unpack" or singularize the list.

A practical example: We have a list of 4, 2-tuple elements:

 my_list = [('a', 232), 
           ('b', 343), 
           ('c', 543), 
           ('d', 23)] 

We want to turn this list into the following 2 element, 4-tuple list:

 [('a', 'b', 'c', 'd'), 
 (232, 343, 543, 23)] 

This can be done by using the *-operator and the zip function in the following way:

 list(zip(*my_list))

Arbitrary Number of Keyword Parameters

In the previous chapter we demonstrated how to pass an arbitrary number of positional parameters to a function. It is also possible to pass an arbitrary number of keyword parameters to a function as a dictionary. To this purpose, we have to use the double asterisk "**"

def f(**kwargs):
    print(kwargs)
f()
{}
f(de="German",en="English",fr="French")
{'de': 'German', 'en': 'English', 'fr': 'French'}

One use case is the following:

def f(a, b, x, y):
    print(a, b, x, y)
d = {'a':'append', 'b':'block','x':'extract','y':'yes'}
f(**d)
append block extract yes

Exercises with Functions

Exercise 1

Write a function which takes a text and encrypts it with a Caesar cipher. This is one of the simplest and most commonly known encryption techniques. Each letter in the text is replaced by a letter some fixed number of positions further in the alphabet.

What about decrypting the coded text?

The Caesar cipher is a substitution cipher.

caesar cipher

Exercise 2

We can create another substitution cipher by permutating the alphet and map the letters to the corresponding permutated alphabet.

Write a function which takes a text and a dictionary to decrypt or encrypt the given text with a permutated alphabet.

Exercise 3

Write a function txt2morse, which translates a text to morse code, i.e. the function returns a string with the morse code.

Write another function morse2txt which translates a string in Morse code into a „normal“ string.

The Morse character are separated by spaces. Words by three spaces.

International Morse Code

Exercise 4

Perhaps the first algorithm used for approximating $\sqrt{S}$ is known as the "Babylonian method", named after the Babylonians, or "Hero's method", named after the first-century Greek mathematician Hero of Alexandria who gave the first explicit description of the method.

If a number $x_n$ is close to the square root of $a$ then $$x_{n+1} = \frac{1}{2}(x_n + \frac{a}{x_n})$$ will be a better approximation.

Write a program to calculate the square root of a number by using the Babylonian method.

Exercise 5

Write a function which calculates the position of the n-th occurence of a string sub in another string s. If sub doesn't occur in s, -1 shall be returned.

Solutions

Solution to Exercise 1

import string
abc = string.ascii_uppercase
def caesar(txt, n, coded=False):
    """ returns the coded or decoded text """
    result = ""
    for char in txt.upper():
        if char not in abc:
            result += char
        elif coded:
            result += abc[(abc.find(char) + n) % len(abc)]
        else:
            result += abc[(abc.find(char) - n) % len(abc)]
    return result

n = 3
x = caesar("Hello, here I am!", n)
print(x)
print(caesar(x, n, True))
EBIIL, EBOB F XJ!
HELLO, HERE I AM!

In the previous solution we only replace the letters. Every special character is left untouched. The following solution adds some special characters which will be also permutated. Special charcters not in abc will be lost in this solution!

import string
abc = string.ascii_uppercase + " .,-?!"
def caesar(txt, n, coded=False):
    """ returns the coded or decoded text """
    result = ""
    for char in txt.upper():
        if coded:
            result += abc[(abc.find(char) + n) % len(abc)]
        else:
            result += abc[(abc.find(char) - n) % len(abc)]
    return result

n = 3
x = caesar("Hello, here I am!", n)
print(x)
print(caesar(x, n, True))
EBIILZXEBOBXFX-J,
HELLO, HERE I AM!

Solution to Exercise 2

import string
from random import sample

alphabet = string.ascii_letters
permutated_alphabet = sample(alphabet, len(alphabet))

encrypt_dict = dict(zip(alphabet, permutated_alphabet))
decrypt_dict = dict(zip(permutated_alphabet, alphabet))

def encrypt(text, edict):
    """ Every character of the text 'text'
    is mapped to the value of edict. Characters
    which are not keys of edict will not change"""
    res = ""
    for char in text:
        res = res + edict.get(char, char)
    return res

# Donald Trump: 5:19 PM, September 9 2014
txt = """Windmills are the greatest 
threat in the US to both bald 
and golden eagles. Media claims 
fictional ‘global warming’ is worse."""

ctext = encrypt(txt, encrypt_dict)
print(ctext + "\n")
print(encrypt(ctext, decrypt_dict))
mkGiMkFFs WAI KqI QAIWKIsK 
KqAIWK kG KqI bp KY wYKq wWFi 
WGi QYFiIG IWQFIs. xIikW gFWkMs 
akgKkYGWF ‘QFYwWF eWAMkGQ’ ks eYAsI.

Windmills are the greatest 
threat in the US to both bald 
and golden eagles. Media claims 
fictional ‘global warming’ is worse.

Solution to Exercise 3

latin2morse_dict = {'A':'.-', 'B':'-...', 'C':'-.-.', 'D':'-..', 
                    'E':'.', 'F':'..-.', 'G':'--.','H':'....', 
                    'I':'..', 'J':'.---', 'K':'-.-', 'L':'.-..', 
                    'M':'--', 'N':'-.', 'O':'---', 'P':'.--.', 
                    'Q':'--.-', 'R':'.-.', 'S':'...', 'T':'-', 
                    'U':'..-', 'V':'...-', 'W':'.--', 'X':'-..-', 
                    'Y':'-.--', 'Z':'--..', '1':'.----', '2':'...--', 
                    '3':'...--', '4':'....-', '5':'.....', '6':'-....', 
                    '7':'--...', '8':'---..', '9':'----.', '0':'-----', 
                    ',':'--..--', '.':'.-.-.-', '?':'..--..', ';':'-.-.-', 
                    ':':'---...', '/':'-..-.', '-':'-....-', '\'':'.----.', 
                    '(':'-.--.-', ')':'-.--.-', '[':'-.--.-', ']':'-.--.-', 
                    '{':'-.--.-', '}':'-.--.-', '_':'..--.-'}

# reversing the dictionary:
morse2latin_dict = dict(zip(latin2morse_dict.values(),
                            latin2morse_dict.keys()))

print(morse2latin_dict)
{'.-': 'A', '-...': 'B', '-.-.': 'C', '-..': 'D', '.': 'E', '..-.': 'F', '--.': 'G', '....': 'H', '..': 'I', '.---': 'J', '-.-': 'K', '.-..': 'L', '--': 'M', '-.': 'N', '---': 'O', '.--.': 'P', '--.-': 'Q', '.-.': 'R', '...': 'S', '-': 'T', '..-': 'U', '...-': 'V', '.--': 'W', '-..-': 'X', '-.--': 'Y', '--..': 'Z', '.----': '1', '...--': '3', '....-': '4', '.....': '5', '-....': '6', '--...': '7', '---..': '8', '----.': '9', '-----': '0', '--..--': ',', '.-.-.-': '.', '..--..': '?', '-.-.-': ';', '---...': ':', '-..-.': '/', '-....-': '-', '.----.': "'", '-.--.-': '}', '..--.-': '_'}
def txt2morse(txt, alphabet):
    morse_code = ""
    for char in txt.upper():
        if char == " ":
            morse_code += "   "
        else:
            morse_code += alphabet[char] + " "
    return morse_code

def morse2txt(txt, alphabet):
    res = ""
    mwords = txt.split("   ")
    for mword in mwords:
        for mchar in mword.split():
            res += alphabet[mchar]
        res += " "
    return res

mstring = txt2morse("So what?", latin2morse_dict)
print(mstring)
print(morse2txt(mstring, morse2latin_dict))
... ---    .-- .... .- - ..--.. 
SO WHAT? 

Solution to Exercise 4

def heron(a, eps=0.000000001):
    """ Approximate the square root of a"""
    previous = 0
    new = 1
    while abs(new - previous) > eps:
        previous = new
        new = (previous + a/previous) / 2
    return new

print(heron(2))
print(heron(2, 0.001))
1.414213562373095
1.4142135623746899

Solution to exercise 5

def findnth(s, sub, n):
    num = 0
    start = -1
    while num < n:
        start = s.find(sub, start+1)
        if start == -1: 
            break
        num += 1
    
    return start

s = "abc xyz abc jkjkjk abc lkjkjlkj abc jlj"
print(findnth(s,"abc", 3))
19