5. Funktionen

Funktionen sind Unterprogramme und helfen die

  • Les- und Wartbarkeit bei großen Programmen zu erleichtern (durch Unterteilung in Unterprogramme) sowie
  • Redundanzen zu vermeiden (wiederkehrende Algorithmen nur 1x implementiert).

Diese

  • benötigen i.A. Eingaben - sog. Parameter (Argumente) - und
  • liefern (damit berechnete) Ergebnisse zurück, genannt Rückgabewerte.

Wir haben bereits Funktionen kennengelernt, z.B. len

In [1]: neue_liste = [3, 5, 9]

In [2]: len(neue_liste)
Out[2]: 3

neue_liste ist dabei ein Parameter, 3 der Rückgabewert.

Den Rückgabewert kann man in eine Variable speichern und weiter verarbeiten mittels:

In [3]: a = len(neue_liste)

In [4]: a
Out[4]: 3

In [5]: print("Laenge=", a)
Laenge= 3

Bei den Sequenzen haben wir auch schon Funktionen von Klassen in Aktion gesehen:

In [7]: s = "Zeichenkette"    # erzeuge Zeichenkettenobjekt s

In [8]: s.count('e')          # zaehle Buchstaben 'e'
Out[8]: 4

Diese werden mit Objektnamen (s) plus Punkt (.) plus Funktionsname (count()) aufgerufen. Genaueres dazu später.

5.1. Erste Schritte

Eine Funktion startet in Python mit dem Schlüsselwort def. Syntaktisch sieht die Definition folgendermaßen aus:

def Funktionsname( Parameterliste ) :
   Anweisung
   ...
   Anweisung
   return Ergebnis    # Optional

Beispiel:

def fak(zahl):
  ergebnis = 1
  for i in range(2, zahl+1):
     ergebnis *= i
  return ergebnis

Dieser Funktion wird die Variable zahl als Parameter im Anweisungskopf übergeben. Der Anweisungskörper berechnet die Fakultät dieser Zahl und gibt das Ergebnis zurück.

Speichert man diesen Quellcode in ein .py File und führt dieses aus, passiert nichts! Warum?

  • Die Funktion wurde zwar eingelesen, aber
  • kein Befehl ruft diese Funktion auf!

Dieses Skript muss entsprechend erweitert werden, damit es die Funktion aufruft (funktion_fak.py):

# Funktion 
def fak(zahl): 
   ergebnis = 1 
   for i in range(2, zahl+1): 
      ergebnis *= i 
   return ergebnis 

# Hauptprogramm
while True: 
    eingabe = int(input("Gib eine Zahl ein: ")) 
    print(fak(eingabe))

Dieses File (z.B. funktion_fak.py) kann man nun:

  • In Spyder laden und ausführen oder
  • in einer Python Shell mit python funktion_fak.py starten oder
  • in der IPython-Konsole ausführen mit runfile("funktion_fak.py") (Voraussetzung man befindet sich in diesem Verzeichnis)

Bemerkung

5.2. Funktionsparameter

5.2.1. Optionale und Schlüsselwortparameter

Parameter können

  • obligatorisch sowie
  • optional sein (mit = in Parameterliste).

Die optionalen Parameter (0 oder mehr) folgen den obligatorischen.

Zum Verständnis ein Beispiel (funktion_add.py):

def add(x, y=5):
    """Gib x plus y zurueck."""
    return x + y

Aufrufe, hier einmal interaktiv:

In [1]: runfile("funktion_add.py")

In [2]: add(4)
Out[2]: 9

In [3]: add(8,3)
Out[3]: 11

Bemerkung

  • runfile führt das file aus, dadurch wird die Funktion definiert und kann verwendet werden.
  • x ist obligatorisch.
  • y ist optional, wird diese Zahl nicht angegeben ist y=5 (Default Wert).

Es gibt unterschiedliche Möglichkeiten des Aufrufes. Zunächst eine Funktionsdefinition:

Beispiel (funktion_sumsub.py):

def sumsub(a, b, c=0, d=0):
    return a - b + c - d

Mögliche Aufrufe:

In [1]: runfile("funktion_sumsub.py")

In [2]: sumsub(12,4)            #1 nur a und b übergeben, Rest = Defaultwerte
Out[2]: 8

In [3]: sumsub(12,4,27,23)      #2 Position optionaler Argumente fix
Out[3]: 12

In [4]: sumsub(12,4,d=23,c=27)  #3 Position optionaler Argumente beliebig!
Out[4]: 12

Da die Position aller Argumente beim 1. und 2. Aufruf fix ist, nennt man diese positional arguments.

Beim 3. Aufruf ist die Position optionaler Argumente beliebig. Die Zuordnung funktioniert über einen Schlüssel (d=, c=). Daher nennt man diese auch Schlüsselwortparameter (keyword arguments)

Hinweis

Der Aufruf #2 ist zu vermeiden da fehleranfällig. Besser ist #3 aber mit gleicher Reihenfolge wie bei der Funktionsdefinition c=27,d=23!

5.2.2. call-by-value / reference

Je nach Programmiersprache gibt es unterschiedliche Möglichkeiten wie Argumente übergeben werden:

  • call-by-value: Hier wird funktionsintern mit Kopien der als Parameter übergebenen Instanzen gearbeitet.

    Vor- Nachteile: Eine Funktion kann keine Änderungen von Instanzen aus dem Hauptprogramm bewirken, erzeugt jedoch unter Umständen einen erheblichen Overhead.

  • call-by-reference: Dabei wird funktionsintern mit Referenzen (Pointer) auf die im Hauptprogramm befindlichen Instanzen gearbeitet.

    Vor- Nachteile: “Schlanke” Calls da keine Daten erzeugt werden, jedoch besteht die Gefahr, dass eine Funktion Instanzen aus dem Hauptprogramm ändern kann.

Python verwendet diesbezüglich eine Mischform.

Eine call-by-reference gibt es nur bei veränderbaren Datentypen (list, dict).

Beispiel:

In [1]: def test(liste):
   ...:     liste += [5,6,7]
   ...:

In [2]: zahlen = [1,2,3]

In [3]: print(zahlen)
[1, 2, 3]

In [4]: test(zahlen)

In [5]: print(zahlen)
[1, 2, 3, 5, 6, 7]

Man sieht die (ungewollte) Veränderung der Liste im Hauptprogramm. Vermeidung einfach durch kopieren beim Funktionsaufruf, d.h.:

In [6]: zahlen = [1,2,3]

In [7]: test(zahlen[:])

In [8]: print(zahlen)
[1, 2, 3]

Bei unveränderbaren Datentypen (z.B. Zahlen) wird ein call-by-value verwendet.

Beispiel:

In [1]: def test_zahl(zahl):
   ...:     zahl += 2
   ...:

In [2]: a = 2

In [3]: print(a)
2

In [4]: test_zahl(a)

In [5]: print(a)
2

Der Grund dafür, man hat in Python versucht die obigen Vor- und Nachteile ideal zu kombinieren.

5.2.3. Docstring

Möchte man eine Funktion dokumentieren, schreibt man direkt nach dem Anweisungskopf einen Kommentar welcher mit """ (dreifaches Anführungszeichen) startet und endet. Den Text dazwischen nennt man docstring.

Beispiel Funktion (funktion_add.py):

def add(x, y=5):
    """Gib x plus y zurueck."""
    return x + y

Den docstring kann man folgendermaßen ausgeben:

In [1]: runfile("funktion_add.py")

In [2]: add.__doc__
Out[2]: 'Gib x plus y zurueck.'

Die magische Funktion __doc__ retourniert den docstring.

5.3. Namensräume

Innerhalb eines Programmes gibt es einen Satz von Variablen. Diese sieht man in Spyder direkt.

Diesen Variablen sind sogenannten Namensräumen zugewiesen in dem diese Werte definiert wurden.

Es gibt eigene Namensräume (namespaces) z.B. innerhalb:

  • Hauptprogramm (global),
  • Funktionskörper (lokal),
  • Klassendefinitionen (lokal, siehe später).
_images/types_namespace-1.png

Die Funktionen globals() und locals() zeigen den Inhalt dieser Namensräume.

Beispiel: Zunächst definieren wir eine Funktion im File (funktion_namespaces.py):

def check():
    x = 123
    print(locals())
    print(globals()) 

und starten den interaktiven Modus:

In [1]: globals()
Out[1]:
{'__name__': '__main__',
 ...
 '_i1': 'globals()'}

In [2]: runfile("funktion_namespaces.py")

In [3]: globals()
Out[3]:
{'__name__': '__main__',
 ...
 'check': <function __main__.check()>,
 '_i3': 'globals()'}

Man sieht folgendes :

  • globals() gibt ein dict zurück. Es gibt schon vordefinierte Wertepaare.
  • Mit runfile wurde aus funktion_namespaces.py die Funkion check geladen (sowie viele andere Dinge).

Dadurch kann diese Funktion erst aufgerufen werden mit:

In [4]: check()
{'x': 123}
{'__name__': '__main__',
 ...
 'check': <function check at 0x000002A7AF5999D8>}

Man sieht folgendes :

  • Der Funktionskörper kennt (auch) globals(), diese sind unverändert (d.h. check() kann sich selbst aufrufen)
  • Weiters kennt der Funktionskörper x (lokale Variable),
  • das Hauptprogramm kennt x aber nicht (keine globale Variable)!
  • ... bedeutet es gibt wesentlich mehr Ausgabe in der Shell als hier dargestellt.

Hier ein Beispiel

In [1]: def test():
   ...:     x = 1      # lokale Variable
   ...:     print(x)
   ...:

In [2]: x=2            # globale Variable

In [3]: test()         # Ausgabe der lokalen Variable x
1

In [4]: x              # Ausgabe der globalen Variable x
Out[4]: 2

Abhilfe schafft eine global Anweisung.

In [1]: def test():
   ...:     global x    # x ist auch im globalen namespace sichtbar
   ...:     x = 1
   ...:     print(x)
   ...:

In [2]: x = 2

In [3]: test()          # ueberschreibt x auch global
1

In [4]: x
Out[4]: 1

Warnung

Diese Art der Programmierung ist nicht gut, weil unübersichtlich und fehleranfällig! Ein typisches Beispiel sind globale Zähler z.B. bei Lizenzabfragen.

Besser ist in diesem Fall ein return Statement:

In [1]: def test():
   ...:     x = 1
   ...:     print(x)
   ...:     return x
   ...:

In [2]: x = 2

In [3]: x = test()          # man sieht wo x ueberschrieben wird
1

In [4]: x
Out[4]: 1

5.4. Vordefinierte Funktionen

Python hat eine Reihe von sogenannten Built-in Functions. Hier eine (unvollständige) Liste:

abs()         bool()            chr()       complex()      dict()
enumerate()   float()           globals()   help()         list()
input()       int()             id()        len()          pow()
locals()      max()             min()       open()
range()       repr()            round()     sorted()
str()         sum()             tuple()     type()

5.5. Übungsbeispiele

Aufgabe 5.1
  1. Implementieren Sie eine Funktion berechne_Distanz(A,B), welche die Distanz zwischen 2 Punkten A und B zurueck gibt. A und B sind Punkte im 3-dimensionalen Raum, die als Liste mit Länge 3 dargestellt werden.
  2. Schreiben Sie eine Funktion minimale_Distanz(Liste), welcher eine Liste von Positionen (dargestellt jeweils als Listen mit Länge 3) übergeben bekommt. Die Funktion sucht den minimalen Abstand zwischen 2 dieser Punkte. Zurückgegeben werden soll eine neue Liste. Diese Liste soll als 1. Wert die minimale Distanz zwischen 2 Punkten und als 2. und 3. Wert die Indizes der dazugehörigen Punkte enthalten.
Aufgabe 5.2
  1. Schreiben Sie eine Funktion Bearbeite mit den Argumenten Dict (ein Dictionary), String (eine Zeichenkette), sowie dem optionalen Argument Kopie mit dem Default Wert False.

  2. Verdoppeln Sie den String in der Funktion.

  3. Unterscheiden Sie:

    • Wenn Kopie den Wert False hat, soll in das Dictionary Dict der Schlüssel 2 mit dem zugehörigen verdoppelten String als Wert eingefügt werden.
    • Wenn Kopie den Wert True hat, soll eine Kopie von dem Dictionary angelegt werden und der Schlüssel 2 mit dem zugehörigen verdoppelten String als Wert in das kopierte Dictionary eingefügt werden.
  4. Geben Sie anschließend das modifizierte Dictionary, sowie den modifizierten String zurück.

  5. Definieren Sie zum Testen Ihrer Funktion im Hauptprogramm das folgenden Dictionary Dict, sowie die folgenden Zeichenkette String:

    Dict = {1 :'Katze', 2:'ElefantElefant',3:'MausMausMaus'}
    String = 'Hund'
    
  6. Rufen Sie Ihre Funktion im Hauptprogramm mit dem Argument Kopie=False auf und testen Sie, ob sich das Dictionary und der String nach dem Funktionsaufruf verändert haben.

  7. Wiederholen Sie die Punkte 5) und 6), aber verwenden Sie diesmal das Argument Kopie=True. Überprüfen Sie erneut, ob sich das Dictionary und der String verändert haben.

  8. Beantworten Sie die folgenden Fragen als Kommentar in Ihrem Skript: Sind Dictionaries bzw. Strings veränderliche oder unveränderliche Datentypen? In welchem Fall wird einer Funktion daher nur der Wert (also call-by-value) und wann das ganze Objekt (also call-by-reference) übergeben?

5.6. weitere Übungsbeispiele

Aufgabe 5.3
Schreiben Sie eine Funktion Negiere(A) mit einem Vektor oder einer Matrize A als Parameter. Die Funktion soll jeden Eintrag von A negieren, d.h. mit -1 multiplizieren. Die Funktion soll das übergebene A nicht verändern, sondern einen neuen Vektor / eine neue Matrize erstellen und zurückgeben.

Vektoren werden hier als Listen und Matrizen als Listen von Listen dargestellt (dabei repräsentiert jede innere Liste jeweils eine Zeile der Matrize).

Hinweis

  • Eine Möglichkeit um zu überpruefen ob es sich bei A um eine Liste oder eine Matrize handelt, ist die Art eines Eintrags z.B. mittels type(A[0]) zu überpruefen.
  • Wenn Sie eine Kopie von einer Matrix anlegen, achten Sie darauf, dass auch die inneren Listen explizit kopiert werden!

Der Aufruf der Funktion könnte wie folgt aussehen:

A = [1,2]
Aneg = Negiere(A)
print(Aneg)    #-> Ausgabe: [-1,-2]
print(A)       #-> Ausgabe: [1,2]
B = [[1.2,2],[3,4.5]]
Bneg = Negiere(B)
print(Bneg)    #-> Ausgabe: [[-1.2,-2],[-3,-4.5]]
print(B)       #-> Ausgabe: [[1.2,2],[3,4.5]]
Aufgabe 5.4

Schreiben Sie 2 Funktionen zur Addition von Matrizen bzw Vektoren. Vektoren sollen dabei als Listen und Matrizen als Listen von Listen dargestellt werden (dabei repräsentiert jede innere Liste jeweils eine Zeile der Matrize).

1.) Eine Funktion AddAssign(A,B), welche A und B addiert und das Ergebnis auf A speichert. Nach erfolgreicher Addition wird True zurückgegeben. D.h. der Aufruf AddAssign(A,B) entspricht A += B.

2.) Eine Funktion Add(A,B), welche A und B addiert und das Ergebnis zurückgibt. A und B sollen sich dabei nicht verändern. D.h. der Aufruf C = Add(A,B) entspricht C = A + B.

Hinweis

Überprüfen Sie zunächst ob die Dimension der Matrizen bzw. Vektoren zusammenpassen und geben Sie False zurück falls das Produkt nicht berechnet werden kann.

Aufgabe 5.5

Schreiben Sie eine Funktion Obstkorb(Fruechte), welche als Parameter einen String Fruechte übergeben bekommt. Fruechte enthält eine beliebige Anzahl an Obstnamen, die durch Strichpunkte (;) getrennt sind. Ein Obstname kann dabei auch mehrmals auftreten.

Die Funktion Obstkorb soll nun ein Dictionary erstellen, mit dem Obstnamen als Schlüssel und der Anzahl der jeweiligen Frucht als Wert. Das Dictionary wird von der Funktion zurückgegeben.

Der Aufruf der Funktion könnte wie folgt aussehen:

Fruechte = "Banane; Apfel; Banane; Birne"
print(Obstkorb(Fruechte))
-> Ausgabe: {'Birne': 1, 'Banane': 2, 'Apfel': 1}
(Die Reihenfolge der Elemente in der Ausgabe ist beliebig.)
Funktionsname: Obstkorb
Parameter: string
Rueckgabewert: Dictionary (keys: strings, values: int)