3.6 Unterprogrammtechnik mit Funktionen
Eine bekannte Problemlösungsstrategie besteht darin, komplexe Probleme in ihre Teilprobleme zu zerlegen. Jedes Teilproblem wird einzeln für sich mit einem Unterprogramm gelöst. Nach dem Programmstart ruft das Hauptprogramm dann diese Unterprogramme in der angegebenen Reihenfolge auf und führt die Berechnungen schrittweise aus. Der Begriff Hauptprogramm wird in Python seltener verwendet als in den anderen verbreiteten Programmiersprachen, z. B. Pascal, C oder C++. Gemeint ist der Teil des Quelltextes, der den Funktionsdefinitionen folgt.
In der Terminologie von Python heißen diese Unterprogramme Funktionen. In der praktischen Informatik sind Funktionen ein wichtiges Mittel, um Programme übersichtlicher zu strukturieren. Einmal erstellte und getestete Funktionen können in anderen Programmen wiederverwendet werden.
Anschaulich kann man eine Python-Funktion als Blackbox mit mehreren Ein- und Ausgängen beschreiben. Die Blackbox fragt die Zustände (Werte) der Eingänge E permanent ab, führt die einprogrammierten Berechnungen aus und gibt die Ergebnisse an die Ausgänge A weiter (siehe Abbildung 3.1).
Abbildung 3.1 Blockschaltbild einer Python-Funktion mit mehreren Rückgabewerten
Bemerkenswert ist, dass eine Python-Funktion auch mehrere Ergebnisse an das Hauptprogramm zurückgeben kann. Das ist keineswegs selbstverständlich, denn andere Programmiersprachen bieten diese Möglichkeit in der Regel nicht. Darauf gehe ich kurz in Abschnitt 3.6.4 ein.
3.6.1 Eingebaute Funktionen
Python stellt 68 eingebaute Funktionen zur Verfügung, von denen Sie nicht genau wissen müssen, wie sie intern funktionieren. Tabelle 3.4 enthält eine Übersicht über einige ausgewählte Python-Funktionen. Diese Funktionen geben nur einen Wert zurück.
Funktion |
Argument |
Beschreibung |
---|---|---|
abs() |
Integer, Float |
Ermittelt den Absolutbetrag des Arguments. |
eval() |
String |
Wertet einen String als mathematischen Ausdruck aus. |
float() |
Zahl oder Zeichenkette |
Konvertiert das Argument in ein Gleitpunktzahlobjekt. |
id() |
Objekt |
Gibt den Integer-Wert Identität des Objekts zurück. |
int() |
Zahl oder Zeichenkette |
Konvertiert das Argument in ein Integer-Objekt. |
input() |
String |
Liest eine Zeichenkette aus der Standardeingabe aus und gibt sie zurück. |
print() |
Objekte |
Gibt Werte aus. |
round(z,n) |
Float |
Rundet eine Gleitpunktzahl z auf n Nachkommastellen. |
str() |
Integer, Float |
Konvertiert das Argument in eine Zeichenkette (String). |
super() |
Objekt |
Gibt ein Objekt zurück, das Methodenaufrufe an eine übergeordnete Klasse delegiert. |
type() |
Objekt |
Gibt den Typ des Objekts zurück. |
Tabelle 3.4 Auswahl eingebauter Python-Funktionen
Eine Übersicht aller eingebauten Funktionen finden Sie unter: https://docs.python. org/3/library/functions.html
Wenn Sie Python-Programme ausführlich analysieren und testen wollen, dann ist die type-Funktion besonders wichtig. Diese Funktion können Sie auch innerhalb der print-Funktion aufrufen. Außerdem können Sie mit type() den Typ eines Objekts für alle Python-Module ermitteln.
3.6.2 Selbst erstellte Funktionen
Natürlich decken die mitgelieferten Funktionen nicht alle Problemstellungen ab. Sie müssen in der Programmierpraxis, je nach Aufgabenstellung, eigene Python-Funktionen definieren, mit denen Sie Probleme lösen. Je strukturierter Sie dabei vorgehen, desto übersichtlicher und verständlicher wird Ihr Programm.
Eine Funktionsdefinition wird mit dem Schlüsselwort def eingeleitet, gefolgt von einem frei wählbaren Bezeichner für den Namen der Funktion. Sie hat den folgenden allgemeinen Aufbau:
def funktionsname(parameter1, prameter2, ...,default=x):
ergebnis1=berechnungen mit den parametern
ergebnis2=berechnungen mit den parametern
...
ergebnisn=berechnungen mit den parametern
return ergebnis1, ergebnis2, .... ergebnisn
Die erste Programmzeile einer Funktionsdefinition muss mit einem Doppelpunkt abgeschlossen werden. In den runden Klammern stehen, durch Kommata getrennt, die formalen Parameter. Als Parameter kann auch ein Defaultwert x vorgegeben werden. Bei einem Funktionsaufruf werden an diesen Positionen die Werte der aktuellen Parameter übergeben. Die innerhalb einer Funktion deklarierten und verwendeten Variablen haben nur eine lokale Gültigkeit, d. h., auf sie kann in einer anderen Funktion oder im Hauptprogramm weder lesend noch schreibend zugegriffen werden.
Der Funktionsbegriff von Python darf nicht mit dem Funktionsbegriff der Mathematik verwechselt werden, obwohl es, wie Sie später sehen werden, durchaus gewisse Ähnlichkeiten gibt.
Funktionen
Eine Python-Funktion ist ein Unterprogramm, das ein Teilproblem löst. Eine Funktionsdefinition besteht aus zwei Teilen: dem Funktionskopf und dem Funktionskörper (oder »Funktionsrumpf«). Der Funktionskopf wird mit dem Schlüsselwort def eingeleitet und endet mit einem Doppelpunkt. Auch wenn die Funktion keine Parameter benötigt, muss der Funktionsname immer mit runden Klammern abgeschlossen werden. Die runden Klammern umschließen die Parameterliste. Funktionsnamen sollten immer mit einem Kleinbuchstaben beginnen. Es gelten die gleichen Regeln wie für Variablenbezeichner.
Der Funktionsrumpf besteht aus einzelnen Anweisungen. Er muss eingerückt werden. Wenn die Funktion einen Wert oder mehrere Werte an das Hauptprogramm zurückgeben soll, muss der Funktionsrumpf mit einer return-Anweisung abgeschlossen werden. Mathematische Operationen können auch direkt hinter der return-Anweisung notiert werden.
Die return-Anweisung hat zwei Aufgaben: Sie übergibt bei einem Funktionsaufruf die von der Funktion berechneten Werte als Objekt an das Hauptprogramm, und sie beendet die Ausführung der Funktion.
3.6.3 Funktionen mit einem Rückgabewert
Listing 3.14 zeigt, wie Python-Funktionen mit einem Rückgabewert definiert und aufgerufen werden. Beim Funktionsaufruf summe=addition(a1,b1) wird die Python-Funktion addition(a1,b1) dem Objekt summe zugewiesen. Das Ergebnis (der Rückgabewert) wird somit in dem Objekt summe gespeichert und kann nun mit der print-Funktion ausgegeben oder an anderer Stelle des Programms benutzt werden. Diese Art von Funktionsaufruf nennt man expliziten Funktionsaufruf.
01 #14_funktion1.py
02 #erste Funktionsdefinition
03 def addition(a,b):
04 return a+b
05 #zweite Funktionsdefinition
06 def multiplikation(a,b):
07 return a*b
08 #Vorgabe der Werte
09 a1,b1=11,42
10 #Funktionsaufrufe
11 summe = addition(a1,b1)
12 produkt = multiplikation(a1,b1)
13 #Ausgabe
14 print("a1 =",a1,"b1 =",b1)
15 print("Addition:", summe)
16 print("Multiplikation:", produkt)
Listing 3.14 Funktionsdefinitionen mit einem Rückgabewert
Ausgabe
a1 = 11 b1 = 42
Addition: 53
Multiplikation: 462
Analyse
In den Zeilen 03 und 04 bzw. 06 und 07 werden die Funktionen addition(a,b) und multiplikation(a,b) definiert. Das Schlüsselwort def leitet die Funktionsdefinition ein. Danach folgt der Bezeichner für den Funktionsnamen. In den runden Klammern stehen die formalen Parameter a und b. Bei einem Funktionsaufruf werden an diesen Positionen die aktuellen Parameter (Argumente) übergeben und für die Berechnungen verwendet. Die Rechenvorschriften stehen direkt hinter dem Schlüsselwort return. Die Funktionen geben die Summe a+b (Zeile 04) und das Produkt a*b (Zeile 07) zurück.
In den Zeilen 11 und 12 werden beide Funktionen aufgerufen und die Argumente a1 und b1 übergeben. Aufgerufen wird eine selbst erstellte Python-Funktion, indem man sie einer Variablen zuweist, also praktisch wie eine gewöhnliche Variable behandelt. In den Objekten summe und produkt sind die Ergebnisse der Addition und Multiplikation abgespeichert.
Bei einem Funktionsaufruf werden die Berechnungen erst dann durchgeführt, wenn die Funktion aufgerufen wird. Das werden Sie feststellen, wenn eine Funktion fehlerhaft programmiert wurde. Wenn z. B. eine Variable durch 0 geteilt wird, stellt der Interpreter diesen Fehler erst fest, wenn die Funktion aufgerufen wird. Testen Sie das Programm mit der fehlerhaften Anweisung return a+b/0 (Zeile 04), und interpretieren Sie die Fehlermeldung.
Nach dem Programmstart können Sie die Funktionen in der Konsole aufrufen:
>>> addition(12,34)
46
Mit dem Debugger von Thonny können Sie den Programmlauf nachvollziehen.
Abbildung 3.2 Der Debug-Button befindet sich direkt rechts neben dem Start-Button.
Klicken Sie auf das grüne Käfer-Icon Debug current script, und klicken Sie anschließend so oft auf Step into ((F7)), bis die Berechnung abgeschlossen ist. Die aktuellen Werte der Variablen werden im Editor an der Position im Quelltext angezeigt, an der der Interpreter die Anweisung auswertet. Bei jedem neuen Funktionsaufruf öffnet sich ein Fenster, in dem die Namen und die Werte der Variablen angezeigt werden.
3.6.4 Funktionen mit mehreren Rückgabewerten
Sie können in Python auch Funktionen definieren, die ein Tupel- oder ein Listenobjekt zurückgeben. In Listing 3.15 wird die Python-Funktion berechnung(a,b) definiert. Sie addiert und multipliziert zwei Zahlen und gibt das Ergebnis als Tupel zurück.
01 #15_funktion2.py
02 def berechnung(a,b):
03 '''Die Funktion berechnet die Summe
04 und das Produkt aus zwei Zahlen.'''
05 s=a+b
06 p=a*b
07 return (s,p)
08 #Vorgabe der Werte
09 a1,b1=11,42
10 #Funktionsaufruf
11 summe,produkt = berechnung(a1,b1)
12 #Ausgabe
13 print("a1 =",a1,"b1 =",b1)
14 print("Summe:", summe)
15 print("Multiplikation:", produkt)
Listing 3.15 Funktion mit mehreren Rückgabewerten
Ausgabe
a1 = 11 b1 = 42
Summe: 53
Multiplikation: 462
Analyse
In den Zeilen 03 und 04 steht ein sogenannter Docstring. Er beschreibt die Aufgabe der Python-Funktion. Mit der eingebauten Python-Funktion help(funktionsname) können Sie sich nach dem Programmstart den Docstring in der Python-Konsole ausgeben lassen:
>>> help(berechnung)
Help on function berechnung in module __main__:
berechnung(a, b)
Die Funktion berechnet die Summe
und das Produkt aus zwei Zahlen.
In den Zeilen 05 und 06 stehen die Rechenoperationen. Die return-Anweisung gibt das Ergebnis der Addition und Multiplikation als Tupel (Zeile 07) zurück. In Zeile 11 erfolgt der Funktionsaufruf durch Zuweisung. Das Ergebnis für die Addition und Multiplikation wird in die Variablen summe und produkt gespeichert.
In Zeile 14 und 15 werden die Ergebnisse ausgegeben.
3.6.5 Namensraum
Ein Namensraum umfasst den Bereich der Gültigkeit von Variablen. Alle Variablen, die als Parameter im Funktionskopf und im Rumpf einer Funktion definiert werden, können durch Variablen mit gleichen Namen, die außerhalb der Funktionsdefinition deklariert wurden, nicht verändert werden. Der folgende Konsolendialog veranschaulicht den Zusammenhang:
>>> def f(a=5):
return a
>>> f()
5
>>> a=10
>>> f()
5
>>> f(11)
11
Der Variablen a wird schon im Funktionskopf ein Standardwert zugewiesen. Dieser Wert kann von außen mit einer Neuzuweisung einer Variablen mit gleichem Namen nicht verändert werden. Wenn der Funktion f(11) ein neues Argument übergeben wird, wird auch der Wert der Variablen a geändert.
Wenn eine Variable vor einer Funktionsdefinition definiert wird, kann diese Variable in der Funktion verwendet werden:
Im ersten Fall wird die Variable a als lokale Variable bezeichnet. Lokale Variablen sind von außen nicht veränderbar. Dieser Schutzmechanismus wird auch als Datenkapselung bezeichnet.
Im zweiten Fall wird die Variable b als globale Variable bezeichnet. Globale Variablen können von allen nachfolgend definierten Funktionen genutzt werden, wenn sie nicht selbst in diesen Funktionen definiert wurden. Um eine Manipulation von außen zu verhindern, sollten Sie eine Funktion für die Berechnung eines Produkts mit def f(a,b):return a*b definieren. Das nächste Beispiel verdeutlicht diese Problematik noch einmal:
c=3
d=4
def f1(a,b): #falsch
s=a+b+c+d
return s
def f2(a,b,c=4,d=5): #richtig
s=a+b+c+d
return s
print(f1(1,2)) #Ausgabe 10
print(f2(1,2)) #Ausgabe 12
Die Werte der Variablen von f2(a,b,c=4,d=5) können durch Variablen mit gleichen Namen und anderen Werten von außen nicht mehr verändert werden.
Hinweis
Vermeiden Sie nach Möglichkeit globale Variablen.
Wenn alle Funktionen Ihres Programms bestimmte Konstanten benötigen, ist eine globale Variablendefinition sinnvoll. Abbildung 3.3 veranschaulicht den Begriff Namensraum.
Die Variablen a und b wurden in der Funktion addition(a,b) definiert. Beim Funktionsaufruf werden ihr die Argumente 11 und 42 übergeben. Das Ergebnis 53 wird der Variablen summe zugewiesen. Die Variablen a und b sind innerhalb des Namensraums der Funktion addition() geschützt. Ihre Werte können von außen nicht geändert werden.
Abbildung 3.3 Namensraum für die Funktion »addition(a,b)«
3.6.6 Rekursion: Eine Funktion ruft sich selbst auf
Es ist möglich, Funktionen zu definieren, die sich selbst aufrufen. Man spricht dann von einer rekursiv definierten Funktion. Eine rekursive Funktion ruft sich so lange selbst auf, bis eine in der Funktion festgelegte Abbruchbedingung erfüllt wird. Wird diese Abbruchbedingung nicht erfüllt, kommt es zu einer Endlosrekursion. Der Interpreter bricht den Aufruf der Funktion mit der Fehlermeldung RecursionError: maximum recursion depth exceeded … ab. Die maximal erlaubte Rekursionstiefe wurde also überschritten. Das Überschreiten der Rekursionstiefe ist mit einer Endlosschleife vergleichbar. Die Anzahl der Rekursionen ist also begrenzt: Mit
>>> import sys
>>> sys.getrecursionlimit()
3000
können Sie die maximal mögliche Rekursionstiefe ermitteln. Dieser Wert wird in der Praxis allerdings nicht erreicht.
Rekursionstiefe
Unter Rekursionstiefe versteht man die Anzahl der Selbstaufrufe einer rekursiv definierten Funktion.
Manchmal sind rekursiv geschriebene Funktionen besser lesbar und verständlicher. Meistens führen sie aber zu höherem Rechenaufwand und längeren Laufzeiten als vergleichbare iterativ geschriebene Algorithmen, die stattdessen Schleifen verwenden.
Das Standardbeispiel für Rekursion ist die Berechnung der Fakultät. Die Berechnungsvorschrift für die Fakultät kann rekursiv definiert werden:

Die Fakultät 5 wird nach dieser Rechenvorschrift wie folgt berechnet:








Das Adjektiv »rekursiv« bedeutet »zurücklaufen« (lat. recurrere). Wenn Sie beispielsweise die Fakultät von 5 berechnen wollen, müssen Sie zuvor die Fakultät von 4 berechnen, dann die Fakultät von 3 usw.
Die rekursive Definition kann direkt in einen leicht lesbaren Python-Quelltext umgesetzt werden (Zeilen 03 bis 06). Die Funktion n*fakultaet(n-1) ruft sich n-mal selbst auf (Zeile 06) und berechnet die Fakultät von n (Listing 3.16):
01 #16_fakultaet_rekursiv.py
02 def fakultaet(n):
03 if n == 0:
04 return 1
05 else:
06 return n*fakultaet(n-1)
07 #Funktionsaufrufe
08 print(fakultaet(5))
09 print(fakultaet(6))
10 print(fakultaet(32))
Listing 3.16 Rekursive Berechnung der Fakultät
Ausgabe
120
720
263130836933693530167218012160000000
Analyse
Beim Funktionsaufruf in Zeile 08 wird der Funktion fakultaet(5) der Zahlenwert 5 übergeben.
In Zeile 03 steht die Abbruchbedingung n==0. Wenn diese Bedingung zutrifft, gibt die Funktion den Wert 1 (Zeile 04) zurück, und die Berechnung wird abgebrochen.
Da n den Wert 5 hat, wird die Anweisung im else-Zweig ausgeführt (Zeile 06).
Die Funktion fakultaet(n-1) ruft sich dort selbst auf. Bei jedem Selbstaufruf der Funktion wird der Wert von n um 1 reduziert, bis die Bedingung n==0 erfüllt ist. Dann gibt die Funktion den Wert 1 zurück und terminiert, führt also keine weiteren Berechnungen mehr aus. Das Argument n-1 ist entscheidend bei einem rekursiven Funktionsaufruf. Wenn sich das Argument nicht mit jedem Selbstaufruf verkleinert, kommt es zu einer Endlosrekursion. Testen Sie das Programm nur mit dem Argument n.
Man könnte annehmen, dass das Programm die Fakultät 5 wie folgt berechnet: 5 ⋅ 4 = 20; 20 ⋅ 3 = 60; 60 ⋅ 2 = 120; 120 ⋅ 1 = 120. Das ist aber nicht der Fall, wie das folgende Testprogramm zeigt:
def fakultaet(n):
print(n)
if n==0: return 1
else:
zw = n*fakultaet(n-1); print(n," ",zw)
return zw
fakultaet(5)
Ausgabe
Zunächst nimmt n in absteigender Reihenfolge die Werte von 5 bis 0 an. Diese Werte werden in einem temporären Speicher zwischengespeichert. In umgekehrter Reihenfolge von 0 bis 5 werden diese Werte der Funktion fakultaet(n-1) bei jedem neuen Funktionsaufruf als Argumente übergeben.
Die Funktion n*fakultaet(n-1) wertet die Ausdrücke in den Klammern von innen nach außen aus:
>>> 5*(4*(3*(2*(1*fakultaet(0)))))
120
Mit dem Debugger von Thonny können Sie sich die einzelnen Rechenschritte anzeigen lassen. Löschen Sie Zeile 07 bis 10, und ersetzen Sie Zeile 07 durch die Anweisung fakultaet(3). Klicken Sie auf das grüne Käfer-Icon Debug current script, und klicken Sie anschließend so oft auf Step into ((F7)), bis die Berechnung abgeschlossen ist.
Mit dem Debugger von Thonny können Sie auch die Einträge des Zwischenspeichers (Heap) analysieren. Veranlassen Sie mit View • Heap, dass das Fenster Heap angezeigt wird. Im Heap-Fenster werden die Identitäten und Speicherinhalte ausgegeben. Klicken Sie auf das grüne Käfer-Icon Debug current script, und klicken Sie anschließend auf Step into ((F7)). Danach klicken Sie auf Resume ((F8)). Im Heap-Fenster erscheint jetzt das Speicherabbild. In der linken Spalte stehen die Identitäten (Speicheradressen, ID), und in der rechten Spalte werden die Werte der Variablen Value aufgelistet.
Abbildung 3.4 Speicherabbild des Heap für n = 5
Für zu große Argumente (etwa n > 990) gibt das Programm folgende Fehlermeldung aus:
RecursionError: maximum recursion depth exceeded in comparison.
Der Fehler wird in Zeile 03 ausgelöst.
Fazit: Rekursive Algorithmen benötigen nur wenige Programmzeilen, der Aufwand der internen Speicherverwaltung ist aber höher als bei der iterativen Variante. Wenn die Programmieraufgabe iterativ lösbar ist, sollten Sie iterative Algorithmen verwenden. Es gibt aber Anwendungsfälle, in denen rekursive Algorithmen die Programmierung erleichtern, wie z. B. bei der Visualisierung von Fraktalen mit der Turtle-Grafik (siehe Kapitel 15).