Parameterübergabe: Parameter und Argumente

Eine Funktion oder eine Prozedur benötigt üblicherweise Information über die Umgebung aus der heraus sie aufgerufen worden ist. Die Schnittstelle zwischen dieser Umgebung und der Funktion bzw. Prozedur, d.h. dem Funktionsrumpf (body) bzw. Prozedurrumpf, besteht aus speziellen Variablen, die man als Parameter bezeichnet. Indem man Parameter benutzt, kann man verschiedenste Objekte von "außen" innerhalb der Funktion benutzen. Die Syntax, wie man Parameter deklariert, und die Semantik, wie man die Argumente des Aufrufs an die formalen Parameter der Funktion oder Prozedur weiterleitet sind abhängig von der jeweiligen Programmiersprache.

Im vorigen Abschnitt haben wir mehrfach die Begriffe Parameter und Argumente verwendet. Bei einigen Lesern hat sich sicherlich der Eindruck eingestellt, dass die beiden Begriffe synonym sind. Auch in der Literatur werden diese Begriffe häufig synonym verwendet. Dennoch gibt es einen klar definierten Unterschied. Parameter stehen im Kopf der Funktion oder Prozedur, also in der Definition der Funktion oder Prozedur. Während Argumente beim Aufruf verwendet werden. Sie liefern die Werte für die formalen Parameter während der Laufzeit des Programmes.

Wertübergabe oder Referenzübergabe

Parameterübergabe als Staffellauf

Die Auswertungsstrategie für Argumente, das heißt wie die Argumente eines Funktionsaufrufes an die formalen Parameter der Funktion übergeben werden, unterscheidet sich von Programmiersprache zu Programmiersprache. Die häufigsten Strategien sind die Wertübergabe (englisch: call by value) und Referenzübergabe (auch Variablenübergabe, englisch: call by reference)

  • Wertübergabe (call by value) Bei der Wertübergabe stellt beim Aufruf einer Funktion jedes Argument einen Wert dar, der ausgewertet wird. Dieser Wert wird dann an den entsprechenden formalen Parameter übergeben, d.h. der Wert des i-ten Ausdrucks (Parameter) wird beim Aufruf dem i-ten Parameter zugewiesen. Die Ausdrücke können aus Literalen, Variablen oder beliebigen Ausdrücke (wie z.B. "a + b + 7" bestehen. Werte von übergebenen Parametern können nicht verändert werden!
  • Referenzübergabe(call by reference) Bei der Referenzübergabe werden, wie der Name impliziert, Referenzen (Speicheradressen) übergeben, d.h. die Speicheradresse des jeweiligen tatsächlichen Parameters. Der formale Parameter wird also zu einem Zeiger auf die Speicheradresse des aufrufenden Parameters. Das bewirkt, dass alle Änderungen an diesem Parameter innerhalb der Funktion sich auch im aufrufenden Teil des Programmes auswirken. Das bedeutet aber auch, dass die tatsächlichen Parameter beim Aufruf nur Ausdrücke sein können, deren Adresse berechnet werden können, also Variablen.

Anmerkung: Die Namensparameter (call by name), wie sie in ALGOL 60 und COBOL genutzt wurde, ist heute nicht mehr üblich.

und wie sieht es bei Python aus?

In vielen Büchern oder Einführungen in Python liest man, dass Python den einen oder den anderen Übergabemechanismus habe, also "call by reference" oder "call by value". Was ist nun richtig?

Humpty Dumpty

Die Erklärung liefert uns Humpty Dumpty:

"Wenn ich ein Wort verwende", erwiderte Humpty Dumpty ziemlich geringschätzig, "dann bedeutet es genau, was ich es bedeuten lasse, und nichts anderes."

"Die Frage ist doch", sagte Alice, "ob du den Worten einfach so viele verschiedene Bedeutungen geben kannst".

"Die Frage ist", sagte Humpty Dumpty, "wer die Macht hat - und das ist alles. [...]"

Lewis Carroll, Through the Looking-Glass (Alles hinter den Spiegeln)

Doch was hat das mit unserer ursprünglichen Frage zu tun: Die Autoren "dehnen" die Begriffe "call-by-value" und "call-by-reference", so dass sie passen, also "dann bedeutet es genau, was ich es bedeuten lasse, und nichts anderes."

Python benutzt einen Mechanismus, den man als "Call-by-Object" (auch "Call by Object Reference" oder "Call by Sharing") bezeichnet.

Parameterübergabe

Wenn man unveränderliche Argumente wie Integers, Strings oder Tuples an eine Funktion übergibt, verhält sich die Übergabe nach außen hin wie eine Wertübergabe. Die Referenz auf das unveränderliche Objekt wird an den formalen Parameter der Funktion übertragen. Innerhalb der Funktion kann der Inhalt des Objektes nicht verändert werden, da es sich ja um unveränderliche Objekte handelt.

Anders verhält es sich jedoch, wenn man veränderliche Argumente überträgt. Auch sie werden per Referenz an die Funktionsparameter übertragen. Allerdings können einzelne Elemente eines solchen veränderlichen Objektes im Funktionsrumpf verändert werden. Wenn wir beispielsweise eine Liste an eine Funktion übergeben, müssen wir zwei Fälle unterscheiden: Die Elemente einer Liste können verändert werden, d.h. diese Änderung wirkt sich auch auf den Gültigkeitsbereich aus, aus dem die Funktion aufgerufen wurde. Wird allerdings ein komplett neues Element dem formalen Parameter zugewiesen, z.B. eine andere Liste, dann hat dies keine Auswirkungen auf die "alte" Liste, also auf die Liste, die beim Aufruf übergeben worden ist.

Zuerst wollen wir einen Blick auf die Integer-Variablen werfen. Der Parameter innerhalb der Funktion ref_demo bleibt solange eine Referenz auf das Objekt, das übergeben wurde, wie keine Änderung am Parameter erfolgt. Sobald also ein neuer Wert an den Parameter überwiesen wird, erzeugt Python eine neue Speicherstelle für das Objekt und der formale Parameter zeigt nun auf diese Speicherstelle. Die Variable des Funktionsaufrufes wird dadurch nicht verändert, wie wir im folgenden Beispiel nachvollziehen können:

def ref_demo(x):
    print("x=",x," id=",id(x))
    x=42
    print("x=",x," id=",id(x))

In dem obigen Beispiel haben wir die Identitätsfunktion id() benutzt, die ein Objekt als Parameter hat. id(obj) liefert die "Identität" des Objektes obj. Diese Identität, der Rückgabewert der Funktion, ist ein Integer, der eindeutig und konstant für dieses Objekt ist, solange es im Programmablauf existiert.

Parameterübergabe

Zwei Objekte, die gleichzeitig existieren müssen verschiedene Identitäten haben. Zwei verschiedene Objekte, die nicht-überlappende Lebensbereiche haben, dürfen allerdings den gleichen Identitätswert haben.

Wenn man die Funktion ref_demo() des obigen Beispiels aufruft, so wie wir es im grünen Block weiter unten tun, können wir prüfen, was mit x passiert: Wir können im Hauptprogramm (main Gültigkeitsbereich) sehen, dass x die Identität zBs. 41902552 hat. In der ersten print-Anweisung der ref_demo()-Funktion, wird das x aus dem main-Gültigkeitsbereich benutzt, denn wir sehen, dass wir den gleichen Identitätswert für x erhalten. Wenn wir jedoch den Wert 42 dem x zuweisen, erhält x eine neue Identität, zBs. 41903752, das heißt eine separate Speicherstelle und das x in main bleibt unberührt, d.h. es hat weiterhin den Wert 9.

Dies bedeutet, dass sich Python zuerst wie Call-by-Reference verhält, aber sobald es einen Wert zugewiesen bekommen, verhält es sich wie bei Call-by-Value. Es wird also eine lokale Speicherposition für den formalen Parameter x angelegt und das x in main behält seinen ursprünglichen Wert:

x = 9
id(x)
Ausgabe: :

140728967733904
ref_demo(x)
x= 9  id= 140728967733904
x= 42  id= 140728967734960
id(x)
Ausgabe: :

140728967733904

Seiteneffekte

Von einem Funktionsaufruf erwartet man, dass die Funktion den korrekten Wert für die Argumente zurückliefert und sonst keine Effekte verursacht, z.B. Ändern von Speicherzuständen. Auch wenn manche Programmierer bewusst Seiteneffekte zur Lösung ihrer Probleme einsetzen, wird dies im Allgemeinen als schlechter Stil betrachtet. Schlimmer ist es jedoch, wenn Seiteneffekte auftreten, mit denen man nicht gerechnet hat, und dadurch Fehler verursacht werden. Dies kann auch in Python passieren. Dies kann dann geschehen, wenn Listen oder Dictionaries als Parameter übergeben werden. Im folgenden Beispiel wird die Liste fib, die als aktueller Parameter an die Funktion s() übergeben wird, mit der Liste [47,11] verkettet.

def s(liste):
     print(id(liste))
     liste += [47,11]
     print(id(liste))
fib = [0,1,1,2,3,5,8]
id(fib)
Ausgabe: :

2364656585160
s(fib)
2364656585160
2364656585160
id(fib)
Ausgabe: :

2364656585160
fib
Ausgabe: :

[0, 1, 1, 2, 3, 5, 8, 47, 11]

Man kann dies verhindern, wenn man statt einer Liste eine Kopie der Liste als Parameter übergibt. Mittels der Slicing Funktion kann man ganz leicht eine Kopie erzeugen. So wird in s(fib[:]) nicht die Liste fib sonder eine komplette Kopie übergeben. Der Aufruf verändert also nicht den Wert von fib, wie man im folgenden interaktiven Programmstück sehen kann:

def s(liste):
     liste += [47,11]
     print(liste)
 
fib = [0,1,1,2,3,5,8]
s(fib[:])
[0, 1, 1, 2, 3, 5, 8, 47, 11]
fib
Ausgabe: :

[0, 1, 1, 2, 3, 5, 8]

Übergabe von Argumenten

Nicht nur Funktionen, sondern auch einem Python-Skript kann man Argumente übergeben. Man bezeichnet diese als Kommandozeilenparameter. Ruft man ein Python-Skript aus einer Shell auf, werden die Argumente jeweils durch ein Leerzeichen voneinander getrennt hinter dem Skriptnamen aufgeführt. Im Python-Skript selber sind die Argumente, also die Kommandozeilenparameter, als Liste unter dem Namen sys.argv abrufbar. Zusätzlich zu den Kommandozeilenparametern enthält diese Liste auch den Namen des aufrufenden Skriptes (Dateiname). Dieser steht als erster Eintrag in der Liste, also sys.argv[0]. Das Skript (argumente.py) listet sämtliche mitgegebenen Argumente in der Standardausgabe auf:

Modul sys wird importiert:

import sys
</pre>

Iteration über sämtliche Argumente:

for eachArg in sys.argv:
print(eachArg) </pre> Beispiel eines Aufrufes, falls das Skript unter argumente.py gespeichert worden ist:

python argumente.py python kurs fuer anfaenger
Dieser Aufruf erzeugt folgende Ausgabe

argumente.py python kurs fuer anfaenger

Variable Anzahl von Parametern

Wir führen nun Funktionen ein, die mit einer beliebige Anzahl von Argumenten aufgerufen werden können. Diejenigen mit Programmiererfahrungen in C und C++ kennen das Konzept als varargs.

Es folgen einige Definitionen, die nicht wichtig für das Verständnis der weiteren Beispiele dieses Kapitels sind: Der Begriff Stelligkeit oder Arität bezeichnet in der Informatik die Parameteranzahl von Funktionen, Prozeduren oder Methoden.

Als variadische Funktion bezeichnet man in der Informatik Funktionen, Prozeduren oder Methoden mit unbestimmter Arität, also solche, deren Parameteranzahl nicht bereits in ihrer Deklaration festgelegt ist.

Ein Sternchen "*" wird in Python benutzt, um eine variable Anzahl von Parametern zu kennzeichnen. Dazu wird das Sternchen unmittelbar vor einem Variablennamen gestellt.

def varpafu(*x): print(x)
varpafu()
varpafu(34,"Magst du Python?", "Natürlich")
()
(34, 'Magst du Python?', 'Natürlich')

Aus dem vorigen Beispiel lernen wir, dass die Argumente, die bei einem Funktionsaufruf von varpafu übergeben werden, in einem Tupel gesammelt werden. Auf dieses Tupel kann als "normale" Variable im Rumpf (body) der Funktion zugegriffen werden. Ruft man die Funktion ohne jegliche Argumente auf, so ist x ein leeres Tupel.

Manchmal ist es notwendig, dass man eine feste Anzahl von Positionsparametern benötigt, die von einer beliebigen Anzahl von Parametern gefolgt werden können. Dies ist möglich, aber die Positionsparameter müssen immer zuerst kommen.

Im folgenden Beispiel benutzen wir einen Positionsparameter "city", der immer angegeben werden muss, gefolgt von einer beliebigen Anzahl von weiteren Städten "andere_städte":

def standorte(stadt, *andere_städte): print(stadt, andere_städte)
standorte("Berlin")
standorte("Berlin","Freiburg","Stuttgart","Konstanz","Frankfurt")
Berlin ()
Berlin ('Freiburg', 'Stuttgart', 'Konstanz', 'Frankfurt')

"*" in Funktionsaufrufen

Ein Stern kann auch in einem Funktionsaufruf erscheinen, wie wir in der vorigen Übung gesehen haben: Die Semantik ist in diesem Fall "invers" zu der Verwendung eines Sternes in der Funktionsdefinition: Ein Argument wird entpackt und nicht gepackt. In anderen Worten: Die Elemente einer Liste oder eines Tuples werden vereinzelt:

def f(x,y,z):
     print(x,y,z)
 
p = (47,11,12)
f(*p)
47 11 12

Es besteht wohl kaum die Notwendigkeit zu erwähnen, dass diese Art unsere Funktion aufzurufen komfortabler als die folgende ist:

f(p[0],p[1],p[2])
47 11 12

Zusätzlich, dass dieser Aufruf weniger komfortabel ist, ist die vorige Aufrufsart im allgemeinen Fall nicht anwendbar, d.h. wenn Listen "unbekannter" Längen verwendet werden sollen. "Unbekannt" bedeutet, dass die Länge erst während der Laufzeit bekannt ist und nicht während man das Skript schreibt. Beliebige Schlüsselwortparameter Es gibt auch einen Mechanismus für eine beliebige Anzahl von Schlüsselwortparametern. Um dies zu ermöglichen wurde als Notation ein doppeltes Sternchen "**" eingeführt:

def f(**args):
     print(args)
 
f()
f(de="deutsch",en="englisch",fr="franzözisch")
{}
{'de': 'deutsch', 'en': 'englisch', 'fr': 'franzözisch'}

Doppeltes Sternchen im Funktionsaufruf

Das folgende Beispiel veranschaulicht die Verwendung von ** in einem Funktionsaufruf:

def f(a,b,x,y):
     print(a,b,x,y)
d = {'a':'append', 'b':'block','x':'extract','y':'yes'}
f(**d)
append block extract yes

und jetzt in Kombination mit *:

t = (47,11)
d = {'x':'extract','y':'yes'}
f(*t, **d)
47 11 extract yes

Übung

Schreibe eine Funktion, die das arithmetische Mittel aus einer variablen Anzahl von Werten berechnet.

Lösung

def arithmetisches_Mittel(x, *l):
    """ Die Funktion berechnet das arithmetische Mittel eines nicht leeren
         beliebige Anzahl von Zahlen """
    sum = x
    for i in l:
        sum += i
    return sum / (1.0 + len(l))

Man mag sich fragen, warum wir sowohl einen Positionsparameter "x" und einen Parameter für eine variable Anzahl von Werten "*l" in unserer Funktionsdefinition benutzt haben. Die Idee besteht darin, dass wir erzwingen wollten, dass unsere Funktion immer mit einer nichtleeren Anzahl von Argumenten aufgerufen wird. Dies ist notwendig, um eine Division durch 0 zu vermeiden, was einen Fehler verursachen würde.

In der folgenden interaktiven Python-Sitzung, lernen wir, wie wir diese Funktion benutzen können. Dazu nehmen wir an, dass die Funktion arithmetic_mean in einer Datei mit dem Namen statistics.py gespeichert worden ist.

arithmetisches_Mittel(4,7,9)
arithmetisches_Mittel(4,7,9,45,-3.7,99)
Ausgabe: :

26.71666666666667

Das funktioniert gut, aber die Sache hat einen Haken. Was, wenn jemand die Funktion mit einer Liste statt mit einer variablen Zahl von Zahlen aufrufen will?

Im Folgenden sehen wir, dass dann ein Fehler verursacht wird:

l = [4,7,9,45,-3.7,99]
arithmetisches_Mittel(l)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-11-19288b78faae> in <module>
      1 l = [4,7,9,45,-3.7,99]
----> 2 arithmetisches_Mittel(l)
<ipython-input-9-4cb70ab2a6ea> in arithmetisches_Mittel(x, *l)
      6         sum += i
      7 
----> 8     return sum / (1.0 + len(l))
TypeError: unsupported operand type(s) for /: 'list' and 'float'

Die Lösung besteht in der Benutzung eines weiteren Sternchens:

arithmetisches_Mittel(*l)
Ausgabe: :

26.71666666666667