4.1. Funktionen#

Wir haben im Laufe der vergangenen Stunden bereits einige Funktionen kennengelernt, zum Beispiel die Ausgabe-Funktion print(), oder die Funktionen enumerate() und range().

4.1.1. Was sind Funktionen?#

Funktionen sind wiederverwendbare Abfolgen von Anweisungen. Sie sind also Codeabschnitte, die mithilfe eines Namens wiederholt ausgeführt werden können. Das Prinzip ist also ein bisschen ähnlich wie Variablen, mit dem Unterschied, dass man mithilfe von Funktionsnamen auf Anweisungen zugreifen kann, während man mithilfe von Variablennamen auf Objekte zugreifen kann. Im Grunde sind Funktionsnamen aber auch zugleich Variablennamen, weil in R Funktionen auch Objekte sind (-> Grundbegriffe: alles in R ist ein Objekt).

Jede Funktion besteht aus drei Komponenten:

  • Name

  • Parameter (in der Funktionsdefinition: formale Parameter)

  • Körper

Funktionen werden aufgerufen. Beim Funktionsaufruf werden ihnen Argumente übergeben. Welche Argumente eine Funktion annehmen kann, wird in der Funktionsdefinition mithilfe von formalen Parametern festgelegt. Für Argumente sagt man deswegen auch manchmal tatsächliche Parameter. Funktionen geben immer einen Wert zurück, der heißt dann Rückgabewert. Der Rückgabewert kann in einer Funktionsdefinition mithilfe des Schlüsselworts return festgelegt werden.

4.1.2. Funktionen definieren#

Funktionen können Nutzer:innen aber auch selbst definieren. Eine Funktionsdefinition hat in Python die allgemeine Form:

Klasse str

Achtung: Zwischen dem Funktionsnamen und der runden Klammer steht KEIN Leerzeichen.

  • mache_irgendwas ist der Funktionsname.

  • Die Funktionsparameter heißen parameter_1 und parameter_2. Dabei handelt es sich um Variablennamen, die nur innerhalb der Funktionsdefinition verwendet werden und als Platzhalter für die Argumente dienen, die der Funktion beim Funktionsaufruf übergeben werden. Die formalen Parameter sind also ein bisschen ähnlich wie die Laufvariablen in for-Schleifen, die auch nur Platzhalter für die Elemente aus einem iterierbaren Objekt waren.

  • Genau wie bei Kontrollstrukturen ist auch bei Funktionen der Funktionskörper alles, was in dem eingeschobenen Block nach dem Doppelpunkt steht, also ein Anweisungsblock und eine return-Anweisung. Im Anweisungsblock stehen irgendwelche Anweisungen, die irgendetwas mit den Parametern der Funktion machen, also im Grunde Verarbeitungsschritte für die Funktionsparameter. Im Laufe der Verarbeitung wird irgendeine Variable definiert, die das “Endergebnis” der Verarbeitung zwischenspeichert. Diese Variable wird dann mithilfe der return-Anweisung zurückgegeben, wenn die Funktion aufgerufen wird. Die return-Anweisung definiert also, welche Variable die Funktion als Rückgabewert beim Funktionsaufruf zurückgeben soll.

Zusätzlich zu diesen unbedingt notwendigen Komponenten, kann eine Funktion eine Spezifikation enthalten. Spezifikationen nennt man docstrings, und sie werden durch drei Anführungszeichen eingegrenzt. Die Funktionsspezifikation erläutert den Nutzer:innen der Funktion, was die Funktion macht, welche Argumente der Funktion beim Funktionsaufruf übergeben werden können und welcher Wert zurückgegeben wird, nachdem die Anweisungen im Funktionskörper ausgeführt worden sind.

Ein Beispiel:

def is_even(i):
    """
    Input: i, a positive int
    Returns True if i is even, otherwise False
    """
    return i%2 == 0
is_even(3)
False

Quelle: Ana Bell 2016.

Für Funktionsnamen gelten laut Style Guide dieselbe Syntaxempfehlungen wie für Variablennamen: Funktionsnamen sollten in Kleinbuchstaben geschrieben werden und einen Unterstrich sollte verwendet werden, um mehrere Wörter voneinander abzutrennen. Allerdings sollten Funktionsnamen immer irgendeine Operation beschreiben, während Variablennamen in der Regel Substantive sind.

Für Docstrings gibt es daneben einen eigenen Style Guide: https://peps.python.org/pep-0257/.

4.1.3. Funktionen aufrufen#

Funktionsaufrufe haben in Python die allgemeine Form:

Klasse str

Achtung: Auch beim Funktionsaufruf steht zwischen dem Funktionsnamen und der runden Klammer KEIN Leerzeichen.

Wenn eine Funktion aufgerufen wird, dann werden die formalen Parameter aus der Funktionsdefinition durch die Argumente (also die tatsächlichen Parameter) ersetzt. Die Verarbeitungsschritte, die im Funktionskörper für die formalen Parameter definiert sind, werden dann mit den Argumenten ausgeführt.

Bisher haben wir bereits oft Funktionen aufgerufen und ihnen Argumente übergeben. Zum Beispiel:

print("Hallo")
Hallo
lst = [1,3,5,2]
len(lst)
4

Wenn wir die Funktion is_even(), die wir vorhin definiert haben, aufrufen wollen, gehen wir genauso vor:

is_even(5)
False

4.1.4. Funktionen verstehen#

In Jupyterlab könnt ihr euch direkt die Spezifikation (Docstring) zu einer bestimmten Funktion anzeigen lassen. So könnt ihr Informationen, welche Argumente der Funktion beim Funktionsaufruf übergeben werden können, und welchen Wert die Funktion nach dem Ausführen der Anweisungen im Funktionsköroer zurückgibt, abrufen, ohne dafür die Funktion zu googeln.

Dazu bewegt ihr den Cursor auf eine Funktion und gebt dann die Tastenkombination Cmd + I bzw. Ctrl + I ein, oder ihr klickt auf Help -> Contextual Help in der Menüleiste oben.

4.1.5. Wozu werden Funktionen verwendet?#

Allgemein werden Funktionen verwendet…

  • …um bestimmte Verarbeitungsschritte zu wiederholen, ohne Code ständig kopieren zu müssen.

  • …um den Code weniger fehleranfällig zu machen: Wenn man den Code kopiert, kopiert man auch mögliche Fehler

Ein Beispiel, wenn es nützlich ist, eine Funktion zu definieren, wäre zum Beispiel die folgende Situation:

Wir wollen die Lieder eine:r Künstler:in analyieren. Hierzu haben wir uns die folgende for-Schleife ausgedacht (eigentlich hat sich allerdings Eric Grimson vom Massachusetts Institute of Technology diese Schleife ausgedacht) :

lyrics = "My mind won't let me rest Voice in my head I hear what it said I can't trust a thing If I picked up and left How fast did you forget? Resting while I'm inside your presence I don't want to think nothing bad This time I won't This time I won't"
lyrics_lst = lyrics.split()
lyrics_dct = {}

for word in lyrics_lst:
    if word in lyrics_dct:
        lyrics_dct[word] += 1
    else:
        lyrics_dct[word] = 1

print(lyrics_dct)
{'My': 1, 'mind': 1, "won't": 3, 'let': 1, 'me': 1, 'rest': 1, 'Voice': 1, 'in': 1, 'my': 1, 'head': 1, 'I': 6, 'hear': 1, 'what': 1, 'it': 1, 'said': 1, "can't": 1, 'trust': 1, 'a': 1, 'thing': 1, 'If': 1, 'picked': 1, 'up': 1, 'and': 1, 'left': 1, 'How': 1, 'fast': 1, 'did': 1, 'you': 1, 'forget?': 1, 'Resting': 1, 'while': 1, "I'm": 1, 'inside': 1, 'your': 1, 'presence': 1, "don't": 1, 'want': 1, 'to': 1, 'think': 1, 'nothing': 1, 'bad': 1, 'This': 2, 'time': 2}

Um die Arbeitsschritte mit mehreren Liedern auszuführen, sähe unser Code so aus:

lyrics_gc = "My mind won't let me rest Voice in my head I hear what it said I can't trust a thing If I picked up and left How fast did you forget? Resting while I'm inside your presence I don't want to think nothing bad This time I won't This time I won't"
lyrics_gc_lst = lyrics_gc.split()
lyrics_gc_dct = {}

for word in lyrics_gc_lst:
    if word in lyrics_gc_dct:
        lyrics_gc_dct[word] += 1
    else:
        lyrics_gc_dct[word] = 1

print(lyrics_gc_dct)

lyrics_dd = "I'm dreamin', ay Truth be told I got the hardest ahead, yeah But I said I never let it get to my head I be in space, in a daze, while you tellin me things I see your face but I never really heard you say it Red light, green light, either I'ma go New place, corner store Ain't that close anymore Yeah let me get the greens, I'll be home by four If you wanna pour up, then I need me a four"
lyrics_dd_lst = lyrics_dd.split()
lyrics_dd_dct = {}

for word in lyrics_dd_lst:
    if word in lyrics_dd_dct:
        lyrics_dd_dct[word] += 1
    else:
        lyrics_dd_dct[word] = 1

print(lyrics_dd_dct)
{'My': 1, 'mind': 1, "won't": 3, 'let': 1, 'me': 1, 'rest': 1, 'Voice': 1, 'in': 1, 'my': 1, 'head': 1, 'I': 6, 'hear': 1, 'what': 1, 'it': 1, 'said': 1, "can't": 1, 'trust': 1, 'a': 1, 'thing': 1, 'If': 1, 'picked': 1, 'up': 1, 'and': 1, 'left': 1, 'How': 1, 'fast': 1, 'did': 1, 'you': 1, 'forget?': 1, 'Resting': 1, 'while': 1, "I'm": 1, 'inside': 1, 'your': 1, 'presence': 1, "don't": 1, 'want': 1, 'to': 1, 'think': 1, 'nothing': 1, 'bad': 1, 'This': 2, 'time': 2}
{"I'm": 1, "dreamin',": 1, 'ay': 1, 'Truth': 1, 'be': 3, 'told': 1, 'I': 7, 'got': 1, 'the': 2, 'hardest': 1, 'ahead,': 1, 'yeah': 1, 'But': 1, 'said': 1, 'never': 2, 'let': 2, 'it': 2, 'get': 2, 'to': 1, 'my': 1, 'head': 1, 'in': 2, 'space,': 1, 'a': 2, 'daze,': 1, 'while': 1, 'you': 3, 'tellin': 1, 'me': 3, 'things': 1, 'see': 1, 'your': 1, 'face': 1, 'but': 1, 'really': 1, 'heard': 1, 'say': 1, 'Red': 1, 'light,': 2, 'green': 1, 'either': 1, "I'ma": 1, 'go': 1, 'New': 1, 'place,': 1, 'corner': 1, 'store': 1, "Ain't": 1, 'that': 1, 'close': 1, 'anymore': 1, 'Yeah': 1, 'greens,': 1, "I'll": 1, 'home': 1, 'by': 1, 'four': 2, 'If': 1, 'wanna': 1, 'pour': 1, 'up,': 1, 'then': 1, 'need': 1}

In der Lösung oben haben wir die for-Schleife einfach kopiert und manuell die Variable lyrics_gc_lst durch die Variable lyrics_dd_lst ersetzt. Das geht bei zwei verschiedenen Liedtexten zwar noch, aber was, wenn wir drei, fünf oder zehn verschiedene Liedtexte haben? Dann produzieren wir extrem viel unnötigen und unübersichtlichen Code, der vielleicht auch noch drei, fünf oder zehnmal denselben Fehler enthält. Eine bessere Lösung ist deswegen hier die Verwendung einer Funktion: die for-Schleife kann damit für alle Liedtexte, die in Python als einfache Zeichenkette repräsentiert werden, verallgemeinert werden:

def lyrics_to_frequencies(lyrics):
    """
    Input: lyrics, a string
    Return a dictionary with each word in the input string as key and the number of occurrences of the word as value.
    """
    lyrics_lst = lyrics.split()
    lyrics_dct = {}

    for word in lyrics_lst:
        if word in lyrics_dct:
            lyrics_dct[word] += 1
        else:
            lyrics_dct[word] = 1
    return lyrics_dct

Die Funktion kann dann mit wechselndem Input aufgerufen werden:

lyrics_gc = "My mind won't let me rest Voice in my head I hear what it said I can't trust a thing If I picked up and left How fast did you forget? Resting while I'm inside your presence I don't want to think nothing bad This time I won't This time I won't"
lyrics_dd = "I'm dreamin', ay Truth be told I got the hardest ahead, yeah But I said I never let it get to my head I be in space, in a daze, while you tellin me things I see your face but I never really heard you say it Red light, green light, either I'ma go New place, corner store Ain't that close anymore Yeah let me get the greens, I'll be home by four If you wanna pour up, then I need me a four"

lyrics_gc_freq = lyrics_to_frequencies(lyrics_gc)
lyrics_dd_freq = lyrics_to_frequencies(lyrics_dd)
print(lyrics_gc_freq)
print(lyrics_dd_freq)
{'My': 1, 'mind': 1, "won't": 3, 'let': 1, 'me': 1, 'rest': 1, 'Voice': 1, 'in': 1, 'my': 1, 'head': 1, 'I': 6, 'hear': 1, 'what': 1, 'it': 1, 'said': 1, "can't": 1, 'trust': 1, 'a': 1, 'thing': 1, 'If': 1, 'picked': 1, 'up': 1, 'and': 1, 'left': 1, 'How': 1, 'fast': 1, 'did': 1, 'you': 1, 'forget?': 1, 'Resting': 1, 'while': 1, "I'm": 1, 'inside': 1, 'your': 1, 'presence': 1, "don't": 1, 'want': 1, 'to': 1, 'think': 1, 'nothing': 1, 'bad': 1, 'This': 2, 'time': 2}
{"I'm": 1, "dreamin',": 1, 'ay': 1, 'Truth': 1, 'be': 3, 'told': 1, 'I': 7, 'got': 1, 'the': 2, 'hardest': 1, 'ahead,': 1, 'yeah': 1, 'But': 1, 'said': 1, 'never': 2, 'let': 2, 'it': 2, 'get': 2, 'to': 1, 'my': 1, 'head': 1, 'in': 2, 'space,': 1, 'a': 2, 'daze,': 1, 'while': 1, 'you': 3, 'tellin': 1, 'me': 3, 'things': 1, 'see': 1, 'your': 1, 'face': 1, 'but': 1, 'really': 1, 'heard': 1, 'say': 1, 'Red': 1, 'light,': 2, 'green': 1, 'either': 1, "I'ma": 1, 'go': 1, 'New': 1, 'place,': 1, 'corner': 1, 'store': 1, "Ain't": 1, 'that': 1, 'close': 1, 'anymore': 1, 'Yeah': 1, 'greens,': 1, "I'll": 1, 'home': 1, 'by': 1, 'four': 2, 'If': 1, 'wanna': 1, 'pour': 1, 'up,': 1, 'then': 1, 'need': 1}

4.1.6. Verschachtelte Schleifen durch Funktionen vereinfachen#

Ein weiterer Fall, wenn es Sinn macht, eine Funktion zu definieren, ist, wenn ein Code ineinandergeschachtelte Schleifen enthält. Wenn Schleifen ineinander verschachtelt sind, kann die äußere Schleife nicht unkompliziert abgebrochen werden, sobald die innere Schleife abgebrochen wird.

Das Problem kann auch ohne Funktion gelöst werden: zum Beispiel, indem eine “Break-Out” Variable verwendet wird, um die äußere Schleife abzubrechen:

# Mit Break-Out Variable
einkaufsliste = ["Tomaten", "Kartoffeln", "Äpfel", "Orangen"]
kilopreis = {"Tomaten": 3.99, "Kartoffeln": 2.99, "Möhren": 0.99, "Äpfel": 2.49, "Orangen": 2.99, "Birnen": 2.19}
einkaufsmenge = {"Tomaten": 0, "Kartoffeln": 0, "Äpfel": 0, "Orangen": 0}
gesamtpreis = 0
grenze = 20
grenze_erreicht = False # Break-Out Variable

while True: # unendliche Schleife
    for artikel in einkaufsliste:
        preis_aktuell = gesamtpreis + kilopreis[artikel]
        if preis_aktuell <= grenze:
            einkaufsmenge[artikel] += 1
            gesamtpreis = preis_aktuell
        else:
            grenze_erreicht = True
            break
    if grenze_erreicht:
        break

print(einkaufsmenge)
print(gesamtpreis)
{'Tomaten': 2, 'Kartoffeln': 2, 'Äpfel': 1, 'Orangen': 1}
19.440000000000005

Beachtet, dass dieser Beispielschcode fehleranfällig und nicht besonders flexibel ist: Die Laufvariable artikel wird verwendet, um drei verschiedene Objekte zu durchlaufen, nämlich einkaufsliste, kilopreis und einkaufsmenge. Das funktioniert nur deswegen, weil alle drei Objekte dieselben Strings als Elemente enthalten. Jedes Mal wurden die Strings manuell eingegeben. Wenn dabei ein Tippfehler passiert, produziert die Schleife nicht mehr den gewünschten Output.

Eine andere Möglichkeit ist die Verwendung der sogenannten “For-Else Syntax”, welche genau für solche Fälle erfunden wurde: Diese Syntax gibt vor, dass der Anweisungsblock unter Else nur dann ausgeführt wird, wenn die innere for-Schleife ohne ein break-Statement terminiert ist.

# Mit For-Else Syntax
einkaufsliste = ["Tomaten", "Kartoffeln", "Äpfel", "Orangen"]
kilopreis = {"Tomaten": 3.99, "Kartoffeln": 2.99, "Möhren": 0.99, "Äpfel": 2.49, "Orangen": 2.99, "Birnen": 2.19}
einkaufsmenge = {"Tomaten": 0, "Kartoffeln": 0, "Äpfel": 0, "Orangen": 0}
gesamtpreis = 0
grenze = 20


while True:
    for artikel in einkaufsliste:
        preis_aktuell = gesamtpreis + kilopreis[artikel]
        if preis_aktuell <= grenze:
            einkaufsmenge[artikel] += 1
            gesamtpreis = preis_aktuell
        else:
            break
    else: # wird nur ausgeführt, wenn die for-Schleife ohne `break` terminiert ist
        continue
    break

print(einkaufsmenge)
print(gesamtpreis)
{'Tomaten': 2, 'Kartoffeln': 2, 'Äpfel': 1, 'Orangen': 1}
19.440000000000005

Die Lösungen mit Break-Out Variable und For-Else-Syntax funktionieren zwar, aber sie erfordern zusätzlichen Code und sind etwas unübersichtlich. Eine Lösung, die übersichtlicher ist und keine zusätzlichen Code-Zeilen und Variablen erfordert, sind Funktionen:

# Als Funktion
einkaufsliste = ["Tomaten", "Kartoffeln", "Äpfel", "Orangen"]
kilopreis = {"Tomaten": 3.99, "Kartoffeln": 2.99, "Möhren": 0.99, "Äpfel": 2.49, "Orangen": 2.99, "Birnen": 2.19}
grenze = 20

def einkauf_planen(einkaufsliste, kilopreis, grenze):
    """
    Berechnet, wie viele Kilogramm von einem Artikel auf der Einkaufsliste in Abhängigkeit vom aktuellen Kilopreis der Artikel und einem vorgegebenen maximalen Einkaufspreis gekauft werden können, und wieviel der Einkauf kostet.

    Parameter:
        einkaufsliste, eine Liste mit Elementen vom Typ String, die Einkaufsartikel repräsentieren
        kilopreis, ein Dictionary mit den Elementen aus der Einkaufsliste als Schlüssel und dem aktuellen Kilopreis der Artikel als Werte
        grenze, ein Integer, der Maximalpreis für den Einkauf in Euro

    Gibt ein Tupel mit einem Dictionary einkaufsmenge und einem Float gesamtpreis als Elemente zurück.
    """
    einkaufsmenge = dict(zip(einkaufsliste, [0]*len(einkaufsliste))) # Elemente der Einkaufsliste als Schlüssel
    gesamtpreis = 0
    while True:
        for artikel in einkaufsliste:
            preis_aktuell = gesamtpreis + kilopreis[artikel]
            if preis_aktuell <= grenze:
                einkaufsmenge[artikel] += 1
                gesamtpreis = preis_aktuell
            else:
                return (einkaufsmenge, gesamtpreis) # Rückgabewert ist ein Tupel!


einkauf = einkauf_planen(einkaufsliste, kilopreis, grenze)
print(einkauf)
({'Tomaten': 2, 'Kartoffeln': 2, 'Äpfel': 1, 'Orangen': 1}, 19.440000000000005)

Eine Änderung, die durch die Funktionsdefinition notwendig geworden ist, ist, dass die Schlüssel des Dictionaries einkaufsmenge aus den Elementen der Einkaufsliste generiert wurden. Das ist hier notwendig, weil die Funktion sonst nur für exakt dieselbe Einkaufsliste verwendet werden könnte (nämlich eine Einkaufsliste mit den Elementen “Tomaten”, “Kartoffeln”, “Äpfel” und “Orangen”. Diese Änderung macht unsere Schleife zugleich deutlich flexibler, denn jetzt müssen nicht dreimal dieselben Strings eingegeben werden, sondern nur zweimal.

Beachtet auch, dass die Funktion hier ein Tupel zurückgibt, das aus den beiden Objekten einkaufsmenge und gesamtpreis zusammengesetzt ist. Auf die einzelnen Elemente kann man ganz regulär mit dem Zugriffsoperator [] für sequentielle Datentypen zugreifen:

einkaufsmenge = einkauf[0]
print(einkaufsmenge)
gesamtpreis = einkauf[1]
print(gesamtpreis)
{'Tomaten': 2, 'Kartoffeln': 2, 'Äpfel': 1, 'Orangen': 1}
19.440000000000005

Note

Tupel können also verwendet werden, wenn eine Funktion mehrere Werte zurückgeben soll.

Wir haben mit der Funktionsdefinition also unseren Code bereits in zwei Aspekten verbessert: Es ist weniger Code notwendig, um auch die äußere Schleife abzubrechen, und wir haben die Variable einkaufsmenge in Abhängigkeit von der Variable einkaufsliste definiert, sodass Änderungen an der Variable einkaufsliste auch direkt auf die Variable einkaufsmenge übertragen werden, ohne, dass wir den Code kopieren müssen. Wie könnte man den Code noch weiter verbessern? Überlegt selbst.

4.1.7. Sichtbarkeitsbereich von Variablen (Variable Scope)#

Wenn wir Variablen erstellt haben, sind wir immer davon ausgegangen, dass diese im gesammten Programm mithilfe des Namens abrufbar sind. Wenn wir selbst eine Funktion definieren, und dabei eine Variable vorkommt, dann ist die im restlichen Programm jedoch im Normalfall nicht abrufbar, sie “existiert” sozusagen nur in der Funktion und ist im restlichen Programm nicht sichtbar. Die Sichtbarkeit von Variablen im eigenen Code lässt sich leicht mithilfe von pythontutor.com nachvollziehen.

Kopiert als Beispiel den Code der Funktion einkauf_planen in PythonTutor und führt ihn dort aus.

4.1.8. Was sind Methoden?#

Wir haben Methoden bisher definiert als die Operationen, die speziell für Objekte eines bestimmten Datentyps definiert sind. Jetzt können wir diese Definition schärfen: Methoden haben genau wie Funktionen einen Namen, Parameter und einen Funktionskörper, und sie geben einen Wert zurück. Aber im Unterschied zu Funktionen sind sie nur für Objekte eines bestimmten Datentyps definiert.

Wir haben bereits den Punkt-Operator . kennengelernt, mit dem Methoden aufgerufen werden können:

wort = "Hallo"
wort.lower()
'hallo'

4.1.9. Quellen#

  1. Ana Bell. Decomposition, Abstraction, and Functions. 2016. URL: https://ocw.mit.edu/courses/6-0001-introduction-to-computer-science-and-programming-in-python-fall-2016/resources/lecture-4-decomposition-abstraction-and-functions/.

  2. David Goodger and Guido van Rossum. PEP 257 - Docstring Conventions. 2022. URL: https://peps.python.org/pep-0257/.

  3. Eric Grimson. Recursion and Dictionaries. 2016. URL: https://ocw.mit.edu/courses/6-0001-introduction-to-computer-science-and-programming-in-python-fall-2016/resources/lecture-6-recursion-and-dictionaries/.

  4. Python 3.11.3 Documentation. Defining Functions. URL: https://docs.python.org/3/tutorial/controlflow.html#defining-functions.

  5. Python 3.11.3 Documentation. Glossary: Functions. URL: https://docs.python.org/3/glossary.html#term-function.