Python: Wie übergibt man eine Variable per Referenz?
Python kennt weder Übergabe per Wert noch per Referenz. Die übliche Erklärung — “veränderliche Typen per Referenz, unveränderliche per Wert” — klingt richtig genug und führt weit genug in die Irre, um sie einmal auseinanderzunehmen.
Die Beobachtung, mit der alle anfangen
def neu_binden(x):
x = 99
def veraendern(x):
x.append(99)
int after rebinding inside a function : 1 <- unchanged
list after .append inside a function : [1, 99] <- changed
Daraus sieht es so aus, als würden Listen und Ganzzahlen unterschiedlich übergeben. Werden sie nicht.
Dieselbe Liste, auf beide Arten
def liste_neu_binden(x):
x = [99]
def liste_veraendern(x):
x.append(99)
after rebind_list(a) : [1] <- unchanged
after mutate_list(a) : [1, 99] <- changed
Gleicher Typ, gleiche Signatur, unterschiedliches Ergebnis. Der Unterschied ist also nicht Liste gegen Ganzzahl, sondern neu binden gegen verändern.
Was tatsächlich passiert
Es wird nichts kopiert. Der Parameter wird zu einem zweiten Namen für dasselbe Objekt:
id(obj) outside : 140234...
id of the parameter : 140234...
same object? : True
Eine Zuweisung innerhalb der Funktion bindet diesen lokalen Namen an etwas anderes. Der Name des Aufrufers bleibt unberührt, denn es war nie derselbe Name — geteilt war nur das Objekt:
id inside : 140234...
id after rebinding inside : 140891... <- a different object
outside is still : 'original'
Man nennt das gelegentlich call by assignment: Ein Argument zu übergeben funktioniert genau wie eine Zuweisung, und eine Zuweisung bindet in Python Namen an Objekte, statt Werte in Kästchen zu kopieren.
Sobald man es so sieht, ergibt sich die Sache mit veränderlich und unveränderlich als Folge und nicht als Regel. Unveränderliche Objekte lassen sich nicht an Ort und Stelle ändern, verändern fällt also aus:
tuple[0] = 9 -> TypeError: 'tuple' object does not support item assignment
str[0] = 'z' -> TypeError: 'str' object does not support item assignment
Es bleibt nur das Neubinden, und das erreicht den Aufrufer nie. Daher der
Anschein einer Wertübergabe bei int, str und tuple.
Wo += aufhört, Sinn zu ergeben
def plus_gleich_liste(x):
x += [99]
def plus_gleich_int(x):
x += 1
list after x += [99] inside : [1, 99] <- CHANGED
int after x += 1 inside : 1 <- unchanged
Dieselbe Syntax, gegenteiliges Ergebnis. += versucht zuerst
__iadd__,
das an Ort und Stelle verändert und dasselbe Objekt zurückgibt. list hat das,
also ist x += [99] gleichbedeutend mit x.extend([99]) und der Aufrufer sieht
es. int hat es nicht, also fällt Python auf x = x + 1 zurück — eine
Neubindung.
+= ist also je nach Typ auf der linken Seite eine Veränderung oder eine
Neubindung. Das sollte man wissen, bevor man es auf einen Parameter anwendet.
Die Falle, die beides tut
Der beste Beleg für das eben Gesagte, und ein wirklich sonderbarer:
t = ([1], 2)
t[0] += [9]
t[0] += [9] on ([1], 2) -> TypeError: 'tuple' object does not support item assignment
...and yet t is now ([1, 9], 2) <- the mutation HAPPENED before the error
Es hat geworfen und funktioniert. Das += auf der inneren Liste ist über
__iadd__ durchgelaufen und hat sie an Ort und Stelle verändert. Danach wollte
Python das Ergebnis in t[0] zurückschreiben, was ein Tupel verweigert — aber
die Liste war bereits geändert.
Wer je einen Beweis dafür wollte, dass += zwei Operationen sind und nicht
eine: hier ist er.
Wie man einen Wert wieder herausbekommt
Es gibt in Python kein & und kein ref. Drei Möglichkeiten, in der
Reihenfolge, in der ich dazu greifen würde.
Gib ihn zurück. Fast immer die richtige Antwort:
b = liefert_neu(b)
return a new value and rebind : [1, 99]
Verändere einen übergebenen Container. In Ordnung, wenn die Aufgabe der Funktion genau darin besteht, etwas zu füllen — macht die Funktion aber schwerer zu überblicken.
Pack ihn in ein Objekt:
class Kiste:
def __init__(self, wert): self.wert = wert
def setze(kiste):
kiste.wert = 99
wrap it in an object : 99
Eine Attributzuweisung verändert das Objekt und bindet keinen Namen neu, sie erreicht den Aufrufer also.
Die Folge, die dir zuerst begegnet
Standardargumente werden einmal ausgewertet, bei der Definition. Ein veränderlicher Standardwert wird damit von allen Aufrufen geteilt:
def sammle(element, in_liste=[]):
in_liste.append(element)
return in_liste
first call : ['a']
second call : ['a', 'b'] <- the SAME list
Die Lösung ist die übliche:
def sammle_ok(element, in_liste=None):
if in_liste is None:
in_liste = []
with None : ['a'] then ['b']
Wenn du nichts teilen willst
list(orig)[0] is orig[0] : True
deepcopy(orig)[0] is orig[0] : False
list(x), x[:] und x.copy() kopieren alle eine Ebene. Wenn die Funktion
etwas Verschachteltes verändern könnte, schützt nur
copy.deepcopy
den Aufrufer.
Hinweis zu Netcup (Werbung)
Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).