Funktionen

Funktionen sind Prozeduren oder, wenn man so will, Subprogramme, die aus dem Hauptprogramm heraus aufgerufen werden. Die Vorteile der Verwendung von Funktionen sind:

  • Funktionen sind wiederverwendbar: Eine einmal geschriebene Funktion kann in einem Programm mehrfach verwendet werden (oder sogar aus unterschiedlichen Programmen heraus)
  • Funktionen verhindern Redundanz
  • Funktionen gliedern den Code und machen ihn so besser verständlich und wartbar.

Eine Funktion schreiben

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()

Funktionsparameter

Die gerade geschriebene Funktion say_hello() stellt nur die Minimalversion einer Funktion dar, die immer dasselbe tut. Wir können eine Funktion flexibler machen, indem wir ihr Parameter zuweisen:


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')

Rückgabewerte

Jede Funktion gibt beim Aufruf einen Wert zurück. Dieser ist, wenn nicht anders angegeben, None.


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')

Ein echtes Beispiel

Erinnern wir uns an die Hausübung, wo wir die beliebtesten Vornamen von 1984 und 2015 verglichen haben. Hier haben wir für jedes der beiden Jahre die entsprechende Datei eingelesen und die Namen in eine Liste eingelesen:


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)

Funktionen vermeiden Redundanz

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.

Eine Funktion mit mehreren Parametern

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

Kommentare

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

Docstrings

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 [ ]:

Mehrzeilige Docstrings

Zusätzlich zur Basisform können auch ausführlichere Docstrings geschrieben werden:


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)

Übung

Die Dichte des Wassers ist abhängig von der Temperatur. Diese Werte stehen im Dictionary densities. Schreiben sie compute_weight so um, dass das Gewicht abhängig von der Temperatur berechnet wird.


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}

Funktionen mit mehreren Rückgabewerten

In den meisten Programmiersprachen können Funktionen nur einen Wert zurückliefern (außer natürlich, wenn die Rückgabewerte in einen Container "verpackt" werden). In Python können aber direkt zwei oder mehr Werte zurückgeliefert werden.


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))

Übung

Schreiben wir eine Funktion char_info(), der ein String übergeben wird und die zwei Werte zurückliefert: len und distinct_len. Wie wollen also die Zahl der Zeichen und die Zahl der unterschiedlichen Zeichen ermitteln.


In [ ]:
# TODO
def char_info(string):
    "Return num of all and num of distinct chars of string."

Default Parameter

Normalerweise muss die Reihenfolge der Argumente beim Aufruf der Funktion der Reihenfolge der Parameter in der Funktionsdeklaration entsprechen und es müssen auch alle Argumente mitgegeben werden:

def set_userdata(username, age):
    ...

set_userdata('otto', 20)

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))

Funktionen mit beliebig vielen Parametern

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)

Gültigkeitsbereich von Variablen

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.