Zum Inhalt

Funktionen

Durch die Nutzung von Funktionen können Programme modularisiert werden. Eine Funktion kapselt dabei einen Programmteil innerhalb einer wiederverwendbaren Einheit. Eine Funktion kann dabei selbst geschrieben werden oder aus einer Bibliothek eingebunden werden. Python hat darüberhinaus auch viele nützliche built-in Funktionen wie etwa print, type und len.

Allgemeiner Aufbau einer Funktion

Eine Funktion wird immer unter einem Funktionsname definiert. Eine Funktionsdefinition wird mit dem Schlüsselwort def eingeführt und mit einem : abgeschlossen. Ebenfalls werden Klammern angegeben (). Die Klammern werden genutzt um Parameter der Funktion zu definieren. Eine Funktion besteht immer aus einem Funktionskopf und einem Funktionskörper.

Im folgenden Beispiel soll eine erste einfache Funktion definiert werden, welche den String "hello world" auf der Kommandozeile ausgibt:

def say_hello():
    print("hello world")

say_hello()  # print: hello world
say_hello()  # print: hello world
say_hello()  # print: hello world

Die Funktion say_hello wird definiert, der Funktionsname ist gänzlich selbstgewählt und könnte auch blablablup heißen. Der Funktionskörper ist dabei eingerückt, da Python whitespace-sensitiv ist. Im Funktionskörper befindet sich der Quellcode, welcher durch den Aufruf der Funktion ausgeführt wird. Die Funktion kann beliebig oft ausgeführt werden. Im Beispiel wird der Aufruf say_hello() dreimal durchgeführt, welcher immer zum selben Ergebnis kommt.

Signatur

Eine Signatur definiert in der Softwareentwicklung die formale Schnittstelle einer Funktion. Diese besteht aus dem Namen, den Parametern und dem Rückgabewert. Für die Nutzung einer Funktion ist es wichtig die Signatur zu kennen. Man muss ja dazu wissen wie die Funktion heißt, welche Parameter diese aufnehmen kann und welchen Rückgabewert diese hat.

Parameter

Eine Funktion kann parametrisiert werden. Eine Funktion kann beliebige Parameter im Funktionskopf angeben, welche beim Aufruf der Funktion gesetzt werden müssen.

def say_hello(name):
    print("hello " + name)

say_hello("students")   # print: hello students
say_hello("hello")      # print: hello hello
say_hello("blub blub")  # print: hello blub blub

Die Funktion say_hello kann mit verschiedenen Parameterbelegungen aufgerufen werden. Der Parameter name ist so definiert, dass er für einen Aufruf der Funktion angegeben werden muss. Ein Aufruf der Funktion say_hello ohne Parameterangabe würde zu einem Fehler führen:

say_hello() 

Folgender Fehler würde bei obigem Aufruf aus dem Programm resultieren:

TypeError: say_hello() missing 1 required positional argument: 'name'

Es gibt die Möglichkeit für Funktionen einen Defaultparameter festzulegen. Falls beim Aufruf einer Funktion für einen entsprechenden Parameter kein Wert festgelegt wird, würde der Defaultparameter gesetzt werden. Im Beispiel wird für den Parameter name der Wert "world" festgelegt:

def say_hello(name="world"):
    print("hello " + name)

say_hello()  # print: hello world

Rückgabewert

Eine Funktion kann über das Schlüsselwort return einen Wert zurückgeben. Im Beispiel ist die Funktion pythagoras definiert, welche auf Basis der Parameter a und b (Seiten des Dreiecks) und des Pythagoräischen Lehrsatzes die Hypotenuse berechnet und diese zurückgibt:

def pythagoras(a, b):
    return (a ** 2 + b ** 2) ** 0.5

c = pythagoras(3, 4)   # 5.0
d = pythagoras(6, 10)  # 11.661903789690601

Funktionen, welche keinen Rückgabewert mittels return definiert haben, geben implizit None als Wert zurück. Beispielsweise die Funktion say_hello, welche oben definiert wurde, hat keinen Rückgabewert definiert:

result = say_hello()  # print: hello world
type(result)          # <class 'NoneType'>

Datentyp None

Programmiersprachen besitzen das Konzept der Null Referenz um, einfach gesprochen, eine leere Variable zu repräsentieren. In Python wird dazu das Schlüsselwort None verwendet. None ist dabei aber nicht gleichzusetzen mit 0, "" oder False, da None wirklich nichts repräsentieren soll. Die Null Referenz hat eine lange Historie und wurde erstmals von Tony Hoare eingführt und gleichsam als The Billion Dollar Mistake betitelt.

Geltungsbereich

Lokale Variablen

Im Funktionskörper können Variablen definiert werden. Diese Variablen werden als lokale oder innere Variablen bezeichnet und sind nur innerhalb der Funktion definiert und gültig. Im folgenden Beispiel wird die Variable hello innerhalb der Funktion definiert:

def say_hello(name="world"):
    hello = "hello " + name + "!"
    return hello

result = say_hello("students")
print(result)  # hello students!

Die Variable hello ist nicht außerhalb der Funktion definiert oder verfügbar. Würde man versuchen die Variable hello außerhalb der Funktion zu nutzen würde dies in einem Fehler resultieren:

def say_hello(name="world"):
    hello = "hello " + name + "!"
    return hello

result = say_hello("students")
print(hello)

Der obige Quellcode würde in einem NameError resultieren, da die Variable hello nicht außerhalb der Funktion definiert ist.

NameError: name 'hello' is not defined

Globale Variablen

Globale Variablen werden außerhalb einer Funktion definiert und liegen im globalen Namensraum. Die Variablen, welche in den bisherigen Beispielen verwendet wurden sind alles globale Variablen. Die Variable a im folgenden Beispiel wäre eine globale Variable:

a = "hello"

Innerhalb einer Funktion kann auf äußere Variablen lesend zugegriffen werden:

a = 10

def add():
    return a + 10

print(add())  # 20

Ein schreibender Zugriff auf eine globale Variable innerhalb einer Funktion ist jedoch nicht direkt möglich:

a = 10

def add():
    a = 20
    return a + 10

print(add())  # 30
print(a)      # 10

Die Anweisung a = 20 legt eine innere Variable a im Geltungsbereich der Funktion an. Diese innere Variable hat den selben Namen wie die globale Variable und überdeckt diese somit. Veränderungen der inneren Variablen wirken sich nicht auf die äußeren Variablen aus. Die äußere Variable a behält somit den Wert 10 bei.

Schlüsselwort global

In Python wurde das Schlüsselwort global eingeführt um innerhalb einer Funktion eine globale Variable anzulegen. Im folgenden Beispiel wird die Variable a als globale Variable angelegt:

def add():
    global a
    a = 10
    return a + 10

print(add())  # 20
print(a)      # 10

Mit dem Aufruf der Funktion add wird die Variable a im globalen Namensraum angelegt und innerhalb des Funktionskörpers wird mit a auf diese globale Variable verwiesen. Die Variable a ist demnach vor den Aufruf der Funktion add nicht definiert. Der folgende Quellcode würde in einen NameError führen:

def add():
    global a
    a = 10
    return a + 10

print(a)

Die Ausführung des obigen Quellcodes würde in folgendem Fehler resultieren:

NameError: name 'a' is not defined

Funktionen in Funktionen aufrufen

Funktionen können innerhalb einer anderen Funktion aufgerufen werden. Im Beispiel werden die Funktionen add und mul definiert. Innerhalb der Funktion example werden diese aufgerufen:

def add(a, b):
    return a + b


def mul(a, b):
    return a * b


def example():
    return add(4, mul(2, 3))


example()  # 10

Funktionen in Funktionen definieren

Ähnlich zur Definition von Variablen in einer Funktionen können auch Funktion in einer Funktion definiert werden. Dabei gilt ähnlich zu Variablen, dass solche Funktion nur im lokalen bzw. inneren Geltungsbereich der äußeren Funktion definiert sind. Im folgenden Beispiel wird die äußere Funktion outer und die innere Funktion inner definiert. Die Funktion inner kann nur innerhalb der Funktion outer genutzt werden.

def outer():
    def inner():
        print("Innen")

    inner()
    print("Außen")

outer()

# print:
# Innen
# Außen

Variablen innerer Funktionen

Ähnlich zum Geltungsbereich von globalen und lokalen Variablen gibt es für verschachtelte Funktionsdefinitionen ähnliche Regeln. Im folgenden Beispiel wird die Variable msg einmal im äußeren und einmal im inneren Geltungsbereich definiert. Die äußere msg Definition bleibt dabei von der inneren msg Definition unberührt:

def outer():
    msg = "Außen"

    def inner():
        msg = "Innen"
        print(msg)

    inner()
    print(msg)

outer()

# print:
# Innen
# Außen

In Python wurde das Schlüsselwort nonlocal eingeführt um Variablen der äußeren Funktion in der inneren Funktion schreibend zugreifbar zu machen. Im folgenden Beispiel wird die Variable msg in der inneren Funktion mit nonlocal definiert. Dadurch entsteht eine Referenz auf die äußere Variable msg. Durch die Anweisung msg = "Innen" wird der Inhalt der äußeren Variable msg überschrieben.

def outer():
    msg = "Außen"

    def inner():
        nonlocal msg
        msg = "Innen"
        print(msg)

    inner()
    print(msg)

outer()

# print:
# Innen
# Innen

global vs nonlocal

Die Nutzung des Schlüsselwort global anstelle von nonlocal in der Funktion inner im obigen Beispiel würde dazu führen, dass eine Variable msg im globalen Geltungsbreich angelegt wird. Die Variable msg, welche in der Funktion outer angelegt wurde, bleibt dadurch unberührt.

Nonlocal in normalen Funktionen

Das Schlüsselwort nonlocal kann nur in inneren Funktionen angewendet werden. Um in einer normalen Funktion eine Variable im äußeren Geltungsbereich anzulegen, muss das Schlüsselwort global verwendet werden, da der globale Geltungsbereich der äußere Kontext einer normalen Funktion ist. Im folgenden Beispiel soll innerhalb der Funktion add eine Variable a im äußeren Geltungsbereich angelegt werden. Mit dem Schlüsselwort nonlocal würde dies zu einem Fehler führen:

def add():
    nonlocal a
    a = 10
    return a + 10

add()
print(a)

Die Ausführung des Quellcodes würde zu folgendem Fehler führen:

SyntaxError: no binding for nonlocal 'a' found

Tiefere Verschachtelung von Funktionen

Grundsätzlich können Funktionen beliebig inneinander verschachtelt werden. Jede Funktion bildet dabei ihren eigenen Geltungsbereich von Variablen. Zu beachten ist dabei auf welchen Geltungsbereich sich das Schlüsselwort nonlocal bezieht. Der Geltungsbereich von nonlocal bezieht sich dabei immer auf den nächst höheren Geltungsbereich in der Hierarchie nach oben. Im Beispiel wird die Funktion inside_inner definiert, dabei wird die Variable msg mit nonlocal definiert. Dadurch referenziert die Variable msg der Funktion inside_inner die Variable msg der Funktion inner.

def outer():
    msg = "Außen"

    def inner():
        msg = "Innen"

        def inside_inner():
            nonlocal msg
            msg = "Innerhalb Innen"
            print(msg)

        inside_inner()
        print(msg)

    inner()
    print(msg)

outer()

# print:
# Innerhalb Innen
# Innerhalb Innen
# Außen

Im obigen Beispiel sind 4 verschachtelte Geltungsbereiche von Variablen definiert. Der äußerste und globale Geltungsbereich und hierarchisch jeweils die inneren Geltungsbereiche der Funktionen:

global
  outer
    inner
      inside_inner

Rekursion

Allgemein wird als Rekursion ein Vorgang bezeichnet, der sich selbst als Teil enthält oder mithilfe von sich selbst definierbar ist. In der Softwareentwicklung wird als Rekursion eine Funktion bezeichnet, welche sich selbst aufruft. Damit sich durch den rekursiven Aufruf keine Endlosschleife bildet, muss zwingend eine Abbruchbedingung definiert werden. Auch in der Mathematik wird eine rekursive Definition gerne verwendet, um einen Sachverhalt elegant zu beschreiben. Dies soll am Beispiel der Fakulät demonstriert werden:

Mathematische Fakultät

Dabei wurde n! so definiert, das der Spezialfall 1! mit dem Wert 1 definiert ist. Dies wäre als Abbruchbedingung zu bezeichen. Für alle anderen Fälle ist die Fakultät (n - 1)! * n wobei (n - 1)! ein rekursiver Aufruf der Fakultät selbst ist. In Python würde die rekursive Fakultät folgendermaßen programmiert werden können:

def fac(n):
    if n <= 1:
        return 1
    else:
        return n * fac(n - 1)

print(fac(5))  # 120

Die Ausführung des Python Quellcodes würde folgendermaßen aussehen:

fac(5)
5 * fac(4)
5 * 4 * fac(3)
5 * 4 * 3 * fac(2)
5 * 4 * 3 * 2 * fac(1)
5 * 4 * 3 * 2 * 1 = 120

Iterative Lösung

Jede rekursive Funktion kann auch mittels einer Iteration umgesetzt werden. Generell ist es so, dass die iterative Lösung meist effizienter ist als die rekurisve Lösung. Die rekursive Lösung eines Problems ist meistens jedoch eleganter und verständlicher. Im folgenden Beispiel wird die Fakultät iterativ gelöst:

def fac(n):
    result = 1
    while n > 1:
        result *= n
        n -= 1

print(fac(5))  # 120

Performanz von rekursiven Lösungen

Ein weiteres Beispiel, welches den Unterschied in der Performanz einer iterativen bzw. einer rekursiven Lösung aufzeigt, sind die Fibonacci-Zahlen. Die Fibonacci-Zahlen haben eine Abbruchbedingung für Werte 0 bzw. 1 definiert. Ansonsten ist die Zahlenfolge so definiert, dass ein Element immer über die Summe der beiden Vorgänger bestimmt wird:

Fibonacci-Zahlen

Die ersten 10 Elemente der Fibonacci-Zahlen sind im folgenden angeführt:

0 1 1 2 3 5 8 13 21 34 ...

Der rekursive Algorithmus zur Berechnung der Fibonacci-Zahlen kann direkt aus der mathematischen Definition übernommen werden und sieht folgendermaßen aus:

def fib(n):
    if n <= 1:
        return 1
    else: 
        return fib(n - 1) + fib(n - 2)

Durch den zweimaligen Aufruf der Funktion in sich selbst ensteht ein Ausführungsbaum. Das bedeutet die Anzahl der Funktionsaufrufe steigt exponentiell an:

Fibonacci Rekursion

Eine iterative Implementierung kann effizienter gestaltet werden und hat dadurch eine deutlich geringere Ausführungszeit:

def fib_iterative(n):
    a, b = 0, 1

    for i in range(n):
        a, b = b, a + b

    return a

Mittels der time Funktion kann die Zeit der jeweiligen Ausführungen gestoppt werden. Mittels dieser Methodik kann ein Vergleich der beiden Implementierungen hinsichtlich der Ausführungszeit durchgeführt werden. Die genaue Funktionsweise der Zeile import time wird im weiteren Verlauf des Skriptums geklärt. Kurz gesagt wird eine externe Funktion eingebunden.

import time

print("+----------+----------+----------+")
print("| fib(n)   | Iterativ | Rekursiv |")
print("+----------+----------+----------+")

for i in range(32, 42):

    start1 = time.time()    
    fib_iterative(i)    
    end1 = time.time()

    start2 = time.time()
    fib(i)
    end2 = time.time()

    print("| {: 8d} | {: 8.2f} | {: 8.2f} |".format(i, end1 - start1, end2 - start2))

print("+----------+----------+----------+")

Die Ausgabe der Berechnung wird in der folgenden Tabelle dargestellt. Die Ausführungszeiten würden bei jedem Rechner etwas anders aussehen. Grundsätzlich wird jedoch deutlich sichtbar, dass die rekursive Ausführung um mehrere Größenordnungen länger dauert.

+----------+----------+----------+
| fib(n)   | Iterativ | Rekursiv |
+----------+----------+----------+
|       32 |     0.00 |     0.65 |
|       33 |     0.00 |     1.05 |
|       34 |     0.00 |     1.89 |
|       35 |     0.00 |     2.85 |
|       36 |     0.00 |     5.34 |
|       37 |     0.00 |     8.86 |
|       38 |     0.00 |    13.47 |
|       39 |     0.00 |    24.70 |
|       40 |     0.00 |    35.06 |
|       41 |     0.00 |    59.44 |
+----------+----------+----------+

Verständnisfrage

Versuchen Sie den Quellcode zu Fibonacci-Zahlen auf Ihrem Rechner auszuführen. Versuchen Sie zu verstehen warum die rekursive Lösung mehr Rechenzeit in Anspruch nimmt.