Funktionen sind Prozeduren oder, wenn man so will, Subprogramme, die aus dem Hauptprogramm heraus aufgerufen werden. Die Vorteile der Verwendung von Funktionen sind:
Eine Funktionsdeklaration beginnt in Python mit dem Schlüsselwort def
(Viele andere Sprachen verwenden statt dessen function
). Nach dem def
folgt der Name der Funktion. Dieser sollte idealerweise ein Verb sein, da eine Funktion immer etwas tut. Der Name der Funktion wird durch ein Paar runde Klammern und einen Doppelpunkt abgeschlossen. Danach folgt der eigentliche Funktionskörper mit dem wiederverwendbaren Code.
In [ ]:
def say_hello():
print('Hello!')
Damit habe wir eine Funktion mit dem Namen say_hello
geschrieben. Die Funktion tut noch nichts, da wir sie noch nicht aufgerufen haben. Der Aufruf der Funktion sieht so aus:
In [ ]:
say_hello()
In [ ]:
def say_hello(username):
print('Hello {}!'.format(username))
Hier legt die Funktionsdeklaration fest, dass der Funktion beim Aufruf ein Wert übergeben werden muss, der dann innerhalb der Funktion als Variable username
verfügbar ist. Wie können diesen Wert als Argument beim Aufruf der Funktion übergeben:
In [ ]:
say_hello('Gunter')
In [ ]:
say_hello('Anna')
In [ ]:
rv = say_hello('Otto')
print('Rückgabewert: {}'.format(rv))
Rückgabewerte sind immer dann sinnvoll, wenn eine Funktion z.B. etwas berechnet und das Resultat der Berechnung im Hauptprogramm verwendet werden soll.
In [ ]:
def shorten(long_str):
rv = long_str
if len(long_str) > 2:
rv = "{}{}{}".format(long_str[0], len(long_str)-2, long_str[-1])
return rv
In [ ]:
shorten('Internationalization')
In [ ]:
shorten('Gunter')
In [ ]:
with open('data/vornamen/vornamen_1984.txt') as fh:
names_84 = [n.rstrip() for n in fh.readlines()]
Anstatt diesen Code für jede zu untersuchende Datei neu zu schreiben, können wir eine entsprechende Funktion programmieren und diese mehrfach aufrufen:
In [ ]:
def read_names(filename):
with open(filename) as fh:
return [n.rstrip() for n in fh.readlines()]
In [ ]:
names_84 = read_names('data/vornamen/vornamen_1984.txt')
names_15 = read_names('data/vornamen/vornamen_2015.txt')
Wenn wir davon ausgehen können, dass wir die beliebtesten Vornamen eines jeden Jahres im Verzeichnis data/vornamen/
finden, und die Dateinamen immer gleich aufgebaut sind (vornamen_YYYY.txt
), können wir auch eine spezialisiertere Funktion schreiben:
In [ ]:
def read_names_for_year(year):
filename = "data/vornamen/vornamen_{}.txt".format(year)
with open(filename) as fh:
return [n.rstrip() for n in fh.readlines()]
In [ ]:
names_84 = read_names_for_year(1984)
Die zweite Lösung ist nicht so allgemein verwendbar wie die erste (weil sie davon ausgeht, dass alle Vornamen-Dateien in einem bestimmten Verzeichnis liegen und einem bestimmten Namensschema folgen), bietet aber neben der kompakteren Schreibweise beim Aufruf einen weiteren Vorteil: Falls sich z.B. an der Verzeichnisstruktur etwas ändert, brauchen wir diese Änderung nicht bei jedem einzelnen Aufruf der Funktion nachziehen, sondern an genau einer Stelle: in der Funktion.
Nehmen wir an, aus irgendwelchen Gründen müssen wir den Verzeichnisnamen von data/vornamen
nach data/popular_firstnames
ändern. Während wir im ersten Beispiel alle Aufrufe von read_names()
suchen und dort den Verzeichnisnamen ändern müßten (was bei zwei Aufrufen jetzt nicht so aufwändig wäre ;-)), brauchen wir im zweiten Fall die Änderung nur einmal (in der Funktion) zu machen:
In [ ]:
def read_names_for_year(year):
filename = "data/popular_firstnames/vornamen_{}.txt".format(year)
with open(filename) as fh:
return [n.rstrip() for n in fh.readlines()]
Hier aber auch gleich eine Warnung: Die Wiederverwendbarkeit einer Funktion hängt stark von ihrer Flexibilität (sprich: Parametrisierbarkeit) ab. Je spezialisierter eine Funktion ist, desto weniger einfach kann sie wiederverwendet werden. Ebenso gilt das Gegenteil: Man kann eine Funktion sehr flexibel schreiben, indem man viele Parameter verwendet, aber irgendwann wird die Verwendung der Funktion dadurch so kompliziert, dass man sie nicht mehr verwenden will.
Grundsätzlich kann eine Funktion beliebig viele Parameter haben. In der Praxis sollte man sich, außer man hat gute Gründe, auf maximal 4 oder 5 Parameter beschränken.
In [ ]:
def compute_weight(length, width, height):
ccm = length * width * height
return ccm / 1000.0
Dokumentation des Source Codes ist wesentlich, weil dadurch der Code erklärt und für spätere Bearbeiter (was auch der ursprüngliche Programmierer sein kann) leichter verständlich wird. Dazu verwendet man Kommentare. Das Kommentarzeichen in Python ist #
. Man sollte allerdings nur Dinge kommentieren, die sich nicht ohnehin einfach aus dem Code ableiten lassen:
...
i += 1 # increase i by 1
...
ist ein gutes Beispiel für einen unnötigen Kommentar.
Im nächsten Beispiel dokumentieren wir, woher ein bestimmter Wert kommt. Hier macht der Kommentar mehr Sinn.
def compute_weight(length, width, height):
ccm = length * width * height
# we assume a water density of 1000 kg/cbm
# so we first convert ccm to cbm and multiply by density
# so ccm / 1000000 * 1000
return ccm / 1000
Python bietet mit Docstrings eine Besonderheit. In Form von Docstrings wird die Dokumentation Teil der Funktion (Docstrings können auch für Module und Pakete verwendet werden, aber dazu kommen wir erst später). Ein Docstring muss unmittelbar nach der Funktionsdeklaration in Form eines Strings erscheinen:
In [ ]:
def compute_weight(length, width, height):
"Return the weight of a fish tank in kg."
ccm = length * width * height
# we assume a water density of 1000 kg/cbm
# so we first convert ccm to cbm and multiply by density
# so ccm / 1000000 * 1000
return ccm / 1000
Der Docstring in diesem Beispiel wird wirklich Teil der Funktion und kann abgefragt we<rden:
In [ ]:
compute_weight.__doc__
Das machen sich beispielsweise die in Python eingebaute help()
-Funktion, aber auch integrierte Entwicklungsumgebungen (IDE) zunutze, um den Benutzern Informationen zu einer Funktion anzubieten.
In [ ]:
help(compute_weight)
Auch in einem Jupyter Notebook steht dies zur Verfügung. Schreiben Sie in der unten stehenden Zelle einen Funktionsnamen (z.B. compute_weight
) und drücken Sie dann gleichzeitig die Tasten Shift
und Tab
.
In [ ]:
In [ ]:
def compute_weight(length, width, height):
"""Compute weight of water (in kg) for a fish tank.
Args:
length -- length in cm.
width -- width in cm.
height -- height in cm.
Returns:
Weight in kg as float.
"""
ccm = length * width * height
# we assume a water density of 1000 kg/cbm
# so we first convert ccm to cbm and multiply by density
# so ccm / 1000000 * 1000
return ccm / 1000
In [ ]:
help(compute_weight)
In [ ]:
# fluid densities only
# ice has a density of 918, steam of 0.59
densities = {0: 999.84, 1: 999.9, 2: 999.94, 3: 999.96, 4: 999.97, 5: 999.96,
6: 999.94, 7: 999.9, 8: 999.85, 9: 999.78, 10: 999.7, 11: 999.6,
12: 999.5, 13: 999.38, 14: 999.24, 15: 999.1, 16: 998.94,
17: 998.77, 18: 998.59, 19: 998.4, 20: 998.2, 21: 997.99,
22: 997.77, 23: 997.54, 24: 997.29, 25: 997.04, 26: 996.78,
27: 996.51, 28: 996.23, 29: 995.94, 30: 995.64, 31: 995.34,
32: 995.02, 33: 994.7, 34: 994.37, 35: 994.03, 36: 993.68,
37: 993.32, 38: 992.96, 39: 992.59, 40: 992.21, 45: 990.21,
50: 988.03, 55: 985.69, 60: 983.19, 65: 980.55, 70: 977.76,
75: 974.84, 80: 971.79, 85: 968.61, 90: 965.3, 95: 961.88,
100: 958.35}
In [ ]:
import random
def get_temperature():
"Return temperature in Celsius and Fahrenheit."""
# as we do not have a thermometer attached, we return a random temperature
celsius = random.randint(-30, 45)
fahrenheit = celsius * (9/5) + 32
return celsius, fahrenheit
celsius, fahrenheit = get_temperature()
print('Aktuelle Temperatur: {}° Celsius ({:0.2f}° Fahrenheit)'.format(celsius, fahrenheit))
In [ ]:
# TODO
def char_info(string):
"Return num of all and num of distinct chars of string."
Falls wir einen Parameter nicht zwingend erwarten, können wir dafür einen Defaultwert definieren.
In [ ]:
def set_userdata(username, age=None):
return username, age
print(set_userdata('otto', 20))
print(set_userdata('anna'))
Wenn der Wert mitgegeben wird, wird er in der Funktion verwendet, falls nicht, wird der Defaultwert verwendet. Solche Parameter müssen nach allen anderen Parametern stehen, weshalb der folgende Code-Block nicht funktioniert.
In [ ]:
def set_userdata(ages=None, username):
return username, age
Wenn mehr als ein Default Parameter definiert wird, können beim Aufruf der Funktion Keyword Arguments verwendet werden, wodurch wir uns nicht mehr an die gegebene Reihenfolge halten müssen:
In [ ]:
def set_userdata(username, age=None, weight=None, height=None):
return username, age, weight, height
print(set_userdata('Otto', height=181))
In [ ]:
print(set_userdata('Otto', height=181, age=25))
Manchmal ist zum Zeitpunkt, an dem die Funktion geschrieben wird, noch nicht bekannt, wie viele Parameter zu erwarten sind. In diesem Fall können wir die Funktionsdeklaration so schreiben:
def my_function(*args):
In diesem Fall werden alle Werte in ein Tupel gepackt:
In [ ]:
def my_function(*args):
print(type(args))
my_function(1, 2, 3)
Damit können wir z.B. eine Funktion zur Ermittlung des arithmetische Mittels schreiben:
In [ ]:
def avg(*args):
return sum(args) / len(args)
avg(1, 2, 3, 4)
Bisher haben wir uns noch keine großen Gedanken darüber gemacht, wann und wo der Wert einer Variablen sichtbar ist. In Zusammenhang mit Funktionen müssen wir uns jedoch damit beschäftigen. Vorauszuschicken ist, dass diese Sichtbarkeit in Python eher ungewöhnlich gelöst ist.
In [ ]:
def increase(val):
val += 1
return val
val = 1
new_val = increase(val)
print(val, new_val)
Wir haben in diesem Beispiel zwei Gültigkeitsbereich für die Variable val
(und damit zwei Variablen): eine global gültige und eine zweite, die nur innerhalb der Funktion sichtbar ist. Wenn wir innerhalb der Funktion den Wert von val
verändern, betrifft das nur das val
innerhalb der Funktion und nicht das der außerhalb gültigen Variable.
Das geht so weit, dass globale Variablen innerhalb einer Funktion nicht verfügbar sind, wenn wir versuchen diese zu verändern:
In [ ]:
def increase():
val += 1
return val
val = 1
new_val = increase()
print(val, new_val)
Das gilt jedoch nur, wenn wir versuchen, die Variable zu verändern. Lesend können wir auf die globale Variable zugreifen:
In [ ]:
def print_val():
print(val)
val = 1
print_val()
Damit wird verhindert, dass der Wert einer globalen Variablen irrtümlich innerhalb einer Funktion verändert wird.
Das eben Behauptete stimmt jedoch nicht uneingeschränkt:
In [ ]:
def compute_final_grade(grades):
grades[1] = 1
return sum(grades) / len(grades)
grades = [1, 5, 2, 1, 3]
print(compute_final_grade(grades))
print(grades)
Wie wir sehen, ändert das Umsetzen eines Wertes einer Liste (eines jeden veränderbaren Datentyps) innerhalb einer Funktion nicht eine lokale Kopie der Liste, sondern den Wert der global definierten Liste. Der Grund dafür ist, dass Container wie Listen oder Dictionaries nicht als Kopie der Originalwerts an die Funktion übergeben werden, sondern als Referenz auf das globale Objekt. grades
innerhalb und außerhalb der Funktion zeigt also auf dasselbe Objekt!
Die beiden Variablen können sogar unterschiedliche Namen haben und zeigen immer noch auf dasselbe Objekt:
In [ ]:
def compute_final_grade(mygrades):
mygrades[1] = 1
return sum(mygrades) / len(mygrades)
grades = [1, 5, 2, 1, 3]
print(compute_final_grade(grades))
print(grades)
Dieses Verhalten kann zu unbeabsichtigten Nebeneffekten und damit zu Fehlern führen, die sehr schwer zu finden sind. Man kann sich am einfachsten dagegen schützen, indem man keine Listen oder Dictionaries als Funktionsargumente verwendet oder zumdindest darauf achtet, dass diese innerhalb der Funktion nicht verändert werden. Alternativ kann man mit Kopien oder Typänderungen auf nicht veränderbare Typen (wie Tupeln) arbeiten. Als Faustregel sollte man aber die Verwendung veränderbarer Typen als Funktionsargumente vermeiden.