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