it-swarm.com.de

Warum erstellt Python nur beim Iterieren einer Liste eine Kopie des einzelnen Elements?

Ich habe das gerade in Python gemerkt, wenn man schreibt

for i in a:
    i += 1

Die Elemente der ursprünglichen Liste a sind tatsächlich überhaupt nicht betroffen, da sich herausstellt, dass die Variable i nur eine Kopie des ursprünglichen Elements in a ist.

Um das ursprüngliche Element zu ändern,

for index, i in enumerate(a):
    a[index] += 1

wäre nötig.

Ich war wirklich überrascht von diesem Verhalten. Dies scheint sehr eingängig zu sein, scheint sich von anderen Sprachen zu unterscheiden und hat zu Fehlern in meinem Code geführt, die ich heute lange Zeit debuggen musste.

Ich habe Python Tutorial) schon einmal gelesen. Um sicher zu gehen, habe ich das Buch gerade noch einmal überprüft und es erwähnt dieses Verhalten überhaupt nicht.

Was ist der Grund für dieses Design? Wird erwartet, dass es eine Standardpraxis in vielen Sprachen ist, so dass das Tutorial glaubt, dass die Leser es natürlich bekommen sollten? In welchen anderen Sprachen ist das gleiche Verhalten bei der Iteration vorhanden, auf das ich in Zukunft achten sollte?

31
xji

Ich habe bereits eine ähnliche Frage beantwortet in letzter Zeit == und es ist sehr wichtig zu erkennen, dass += Verschiedene Bedeutungen haben kann:

  • Wenn der Datentyp eine direkte Addition implementiert (d. H. Eine korrekt funktionierende __iadd__ - Funktion hat), werden die Daten, auf die sich i bezieht, aktualisiert (unabhängig davon, ob sie sich in einer Liste oder an einer anderen Stelle befinden).

  • Wenn der Datentyp keine __iadd__ - Methode implementiert, ist die i += x - Anweisung nur syntaktischer Zucker für i = i + x, Daher wird ein neuer Wert erstellt und dem Variablennamen i zugewiesen.

  • Wenn der Datentyp __iadd__ Implementiert, aber etwas Seltsames tut. Es ist möglich, dass es aktualisiert wird ... oder nicht - das hängt davon ab, was dort implementiert ist.

Pythons-Ganzzahlen, Floats und Strings implementieren __iadd__ Nicht, sodass diese nicht direkt aktualisiert werden. Andere Datentypen wie numpy.array Oder lists implementieren es jedoch und verhalten sich wie erwartet. Es geht also beim Kopieren nicht um Kopieren oder Nicht-Kopieren (normalerweise werden keine Kopien für lists und Tuples erstellt - dies hängt jedoch auch von der Implementierung der Container __iter__ Und __getitem__ Ab. Methode!) - Es ist eher eine Frage des Datentyps, den Sie in Ihrem a gespeichert haben.

68
MSeifert

Klarstellung - Terminologie

Python unterscheidet nicht zwischen den Konzepten des Referenzzeigers und des Zeigers . Sie verwenden normalerweise nur den Begriff Referenz , aber wenn Sie mit Sprachen wie C++ vergleichen, die diese Unterscheidung haben, ist es viel näher an einer Zeiger .

Da der Fragesteller eindeutig aus dem C++ - Hintergrund stammt und diese Unterscheidung - die für die Erklärung erforderlich ist - existiert nicht in Python, habe ich mich für die Verwendung der C++ - Terminologie entschieden:

  • Wert : Aktuelle Daten, die sich im Speicher befinden. void foo(int x); ist eine Signatur einer Funktion, die eine Ganzzahl nach Wert empfängt.
  • Zeiger : Eine als Wert behandelte Speicheradresse. Kann verschoben werden, um auf den Speicher zuzugreifen, auf den es zeigt. void foo(int* x); ist eine Signatur einer Funktion, die eine Ganzzahl per Zeiger empfängt.
  • Referenz : Zucker um Zeiger. Es gibt einen Zeiger hinter den Kulissen, aber Sie können nur auf den verzögerten Wert zugreifen und die Adresse, auf die er zeigt, nicht ändern. void foo(int& x); ist eine Signatur einer Funktion, die eine Ganzzahl erhält als Referenz.

Was meinst du mit "anders als in anderen Sprachen"? Die meisten Sprachen, von denen ich weiß, dass sie für jede Schleife Unterstützung bieten, kopieren das Element, sofern nicht ausdrücklich anders angegeben.

Speziell für Python (obwohl viele dieser Gründe für andere Sprachen mit ähnlichen architektonischen oder philosophischen Konzepten gelten können):

  1. Dieses Verhalten kann Fehler für Personen verursachen, die sich dessen nicht bewusst sind, aber das alternative Verhalten kann Fehler verursachen selbst für diejenigen, die sich dessen bewusst sind davon. Wenn Sie eine Variable zuweisen (i), halten Sie normalerweise nicht an und berücksichtigen alle anderen Variablen, die dadurch geändert würden (a). Die Einschränkung des Bereichs, an dem Sie arbeiten, ist ein wichtiger Faktor, um Spaghetti-Code zu verhindern. Daher ist die Iteration durch Kopieren normalerweise die Standardeinstellung, selbst in Sprachen, die die Iteration durch Referenz unterstützen.

  2. Python-Variablen sind immer ein einzelner Zeiger, daher ist das Iterieren durch Kopieren billig - billiger als das Iterieren durch Referenz, was bei jedem Zugriff auf den Wert eine zusätzliche Verzögerung erfordern würde.

  3. Python hat nicht das Konzept von Referenzvariablen wie beispielsweise C++. Das heißt, alle Variablen in Python sind tatsächlich Referenzen, aber in dem Sinne, dass sie Zeiger sind - keine Konstat-Referenzen hinter den Kulissen wie C++ type& name Argumente. Da dieses Konzept in Python nicht vorhanden ist, wird die Iteration als Referenz implementiert - geschweige denn als Standard! - erfordert mehr Komplexität für den Bytecode.

  4. Die Anweisung for von Python funktioniert nicht nur für Arrays, sondern auch für ein allgemeineres Konzept von Generatoren. Hinter den Kulissen ruft Python iter auf Ihren Arrays auf, um ein Objekt zu erhalten, das - wenn Sie next darauf aufrufen - entweder das nächste Element oder zurückgibt raises a StopIteration. Es gibt verschiedene Möglichkeiten, Generatoren in Python zu implementieren, und es wäre viel schwieriger gewesen, sie für die Iteration nach Referenz zu implementieren.

19
Idan Arye

Keine der Antworten hier gibt Ihnen einen Code, mit dem Sie arbeiten können, um warum zu veranschaulichen. Dies geschieht in Python land. Und es macht Spaß, dies genauer zu betrachten tiefe Annäherung also hier geht.

Der Hauptgrund dafür, dass dies nicht wie erwartet funktioniert, ist, dass Sie in Python beim Schreiben Folgendes tun:

i += 1

es tut nicht das, was du denkst. Ganzzahlen sind unveränderlich. Dies können Sie sehen, wenn Sie sich ansehen, was das Objekt in Python tatsächlich ist:

a = 0
print('ID of the first integer:', id(a))
a += 1
print('ID of the first integer +=1:', id(a))

Die ID-Funktion repräsentiert einen eindeutigen und konstanten Wert für ein Objekt in seiner Lebensdauer. Konzeptionell wird es lose einer Speicheradresse in C/C++ zugeordnet. Ausführen des obigen Codes:

ID of the first integer: 140444342529056
ID of the first integer +=1: 140444342529088

Dies bedeutet, dass das erste a nicht mehr mit dem zweiten a identisch ist, da ihre IDs unterschiedlich sind. Tatsächlich befinden sie sich an verschiedenen Stellen im Speicher.

Bei einem Objekt funktionieren die Dinge jedoch anders. Ich habe das += Operator hier:

class CustomInt:
  def __iadd__(self, other):
    # Override += 1 for this class
    self.value = self.value + other.value
    return self

  def __init__(self, v):
    self.value = v

ints = []
for i in range(5):
  int = CustomInt(i)
  print('ID={}, value={}'.format(id(int), i))
  ints.append(int)


for i in ints:
  i += CustomInt(i.value)

print("######")
for i in ints:
  print('ID={}, value={}'.format(id(i), i.value))

Wenn Sie dies ausführen, erhalten Sie die folgende Ausgabe:

ID=140444284275400, value=0
ID=140444284275120, value=1
ID=140444284275064, value=2
ID=140444284310752, value=3
ID=140444284310864, value=4
######
ID=140444284275400, value=0
ID=140444284275120, value=2
ID=140444284275064, value=4
ID=140444284310752, value=6
ID=140444284310864, value=8

Beachten Sie, dass das id-Attribut in diesem Fall für beide Iterationen tatsächlich dasselbe ist, obwohl der Wert des Objekts unterschiedlich ist (Sie können auch das id des int-Werts finden das Objekt hält, was sich ändern würde, wenn es mutiert - weil ganze Zahlen unveränderlich sind).

Vergleichen Sie dies damit, wenn Sie dieselbe Übung mit einem unveränderlichen Objekt ausführen:

ints_primitives = []
for i in range(5):
  int = i
  ints_primitives.append(int)
  print('ID={}, value={}'.format(id(int), i))

print("######")
for i in ints_primitives:
  i += 1
  print('ID={}, value={}'.format(id(int), i))


print("######")
for i in ints_primitives:
  print('ID={}, value={}'.format(id(i), i))

Dies gibt aus:

ID=140023258889248, value=0
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4
######
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4
ID=140023258889408, value=5
######
ID=140023258889248, value=0
ID=140023258889280, value=1
ID=140023258889312, value=2
ID=140023258889344, value=3
ID=140023258889376, value=4

Ein paar Dinge hier zu beachten. Zuerst in der Schleife mit dem +=, Sie fügen nicht mehr zum ursprünglichen Objekt hinzu. In diesem Fall, weil Ints zu den unveränderlichen Typen in Python gehören, python verwendet eine andere ID. Interessant ist auch, dass Python = verwendet dasselbe zugrunde liegende id für mehrere Variablen mit demselben unveränderlichen Wert:

a = 1999
b = 1999
c = 1999

print('id a:', id(a))
print('id b:', id(b))
print('id c:', id(c))

id a: 139846953372048
id b: 139846953372048
id c: 139846953372048

tl; dr - Python hat eine Handvoll unveränderlicher Typen, die das angezeigte Verhalten verursachen. Für alle veränderlichen Typen Ihre Erwartung ist richtig.

11
enderland

Die Antwort von @ Idan erklärt sehr gut, warum Python behandelt die Schleifenvariable nicht wie in C als Zeiger, aber es lohnt sich, ausführlicher zu erklären, wie die Codefragmente entpackt werden, als in Python viele einfach erscheinende Codebits werden tatsächlich Aufrufe von eingebaute Methoden sein. Um Ihr erstes Beispiel zu nehmen

for i in a:
    i += 1

Es gibt zwei Dinge zu entpacken: die Syntax for _ in _: Und die Syntax _ += _. Um die for-Schleife zuerst zu nehmen, wie in anderen Sprachen Python hat eine for-each - Schleife, die im Wesentlichen Syntaxzucker für ein Iteratormuster ist. In Python ist ein Iterator ein Objekt, das a definiert .__next__(self) Methode, die das aktuelle Element in der Sequenz zurückgibt, zum nächsten übergeht und ein StopIteration auslöst, wenn die Sequenz keine weiteren Elemente enthält Iterable ist ein Objekt, das eine .__iter__(self) -Methode definiert, die einen Iterator zurückgibt.

(NB.: Ein Iterator ist auch ein Iterable und kehrt von seiner .__iter__(self) -Methode zurück.)

Python verfügt normalerweise über eine integrierte Funktion, die an die benutzerdefinierte Methode mit doppeltem Unterstrich delegiert. Es hat also iter(o) , das in o.__iter__() und next(o) aufgelöst wird, das in o.__next__() aufgelöst wird. Beachten Sie, dass diese integrierten Funktionen häufig eine vernünftige Standarddefinition versuchen, wenn die Methode, an die sie delegieren würden, nicht definiert ist. Zum Beispiel wird len(o) normalerweise in o.__len__() aufgelöst, aber wenn diese Methode nicht definiert ist, wird iter(o).__len__() versucht.

Eine for-Schleife wird im Wesentlichen durch next(), iter() und grundlegendere Steuerstrukturen definiert. Im Allgemeinen der Code

for i in %EXPR%:
    %LOOP%

wird zu so etwas ausgepackt

_a_iter = iter(%EXPR%)
while True:
    try:
        i = next(_a_iter)
    except StopIteration:
        break
    %LOOP%

Also in diesem Fall

for i in a:
    i += 1

wird ausgepackt zu

_a_iter = iter(a) # = a.__iter__()
while True:
    try: 
        i = next(_a_iter) # = _a_iter.__next__()
    except StopIteration:
        break
    i += 1

Die andere Hälfte davon ist i += 1. Im Allgemeinen wird %ASSIGN% += %EXPR% In %ASSIGN% = %ASSIGN%.__iadd__(%EXPR%) entpackt. Hier führt __iadd__(self, other) eine Addition durch und gibt sich selbst zurück.

(Hinweis: Dies ist ein weiterer Fall, in dem Python wählt eine Alternative, wenn die Hauptmethode nicht definiert ist. Wenn das Objekt __iadd__ Nicht implementiert, wird auf __add__. Dies geschieht in diesem Fall tatsächlich, da int__iadd__ Nicht implementiert - was sinnvoll ist, da sie unveränderlich sind und daher nicht an Ort und Stelle geändert werden können.)

Ihr Code hier sieht also so aus

_a_iter = iter(a)
while True:
    try:
        i = next(_a_iter)
    except StopIteration:
        break
    i = iadd(i,1)

wo wir definieren können

def iadd(o, v):
    try:
        return o.__iadd__(v)
    except AttributeError:
        return o.__add__(v)

In Ihrem zweiten Code ist etwas mehr los. Die zwei neuen Dinge, die wir wissen müssen, sind, dass %ARG%[%KEY%] = %VALUE% In (%ARG%).__setitem__(%KEY%, %VALUE%) Entpackt wird und %ARG%[%KEY%] In (%ARG%).__getitem__(%KEY%) Entpackt wird. Wenn wir dieses Wissen zusammenfassen, wird a[ix] += 1 In a.__setitem__(ix, a.__getitem__(ix).__add__(1)) entpackt (wieder: __add__ Statt __iadd__, Da __iadd__ Nicht von Ints implementiert wird ). Unser endgültiger Code sieht so aus:

_a_iter = iter(enumerate(a))
while True:
    try:
        index, i = next(_a_iter)
    except StopIteration:
        break
    a.__setitem__(index, iadd(a.__getitem__(index), 1))

Um Ihre Frage zu beantworten, warum der erste die Liste nicht ändert, während der zweite dies tut, erhalten wir in unserem ersten Snippet i von next(_a_iter), was i wird ein int sein. Da int nicht an Ort und Stelle geändert werden kann, ändert i += 1 Nichts an der Liste. In unserem zweiten Fall ändern wir erneut nicht das int, sondern die Liste durch Aufrufen von __setitem__.

Der Grund für diese ganze aufwändige Übung ist, dass sie meiner Meinung nach die folgende Lektion über Python lehrt:

  1. Der Preis für Pythons Lesbarkeit ist, dass diese magischen Double-Score-Methoden ständig aufgerufen werden.
  2. Um die Chance zu haben, einen Teil von Python Code) wirklich zu verstehen, müssen Sie diese Übersetzungen verstehen.

Die doppelten Unterstrichmethoden sind eine Hürde beim Start, aber sie sind wichtig, um Pythons Ruf als "ausführbarer Pseudocode" zu unterstützen. Ein anständiger Python -Programmierer hat ein gründliches Verständnis dieser Methoden und wie sie aufgerufen werden, und definiert sie, wo immer es sinnvoll ist, dies zu tun.

Bearbeiten : @deltab hat meine schlampige Verwendung des Begriffs "Sammlung" korrigiert.

6
walpen

+= funktioniert anders, je nachdem, ob der aktuelle Wert veränderlich oder nveränderlich ist. Dies war der Hauptgrund dafür, dass es lange dauern wird, bis es in Python implementiert wird, da Python Entwickler befürchteten, dass es verwirrend sein würde.

Wenn i ein int ist, kann es nicht geändert werden, da Ints unveränderlich sind. Wenn sich also der Wert von i ändert, muss es notwendigerweise auf ein anderes Objekt verweisen:

>>> i=3
>>> id(i)
14336296
>>> i+=1
>>> id(i)
14336272   # Other object

Wenn die linke Seite jedoch veränderlich ist, kann + = sie tatsächlich ändern. wie wenn es eine Liste ist:

>>> i=[]
>>> id(i)
140257231883944
>>> i+=[1]
>>> id(i)
140257231883944  # Still the same object!

In Ihrer for-Schleife bezieht sich i nacheinander auf jedes Element von a. Wenn dies ganze Zahlen sind, gilt der erste Fall und das Ergebnis von i += 1 muss sein, dass es sich auf ein anderes ganzzahliges Objekt bezieht. Die Liste a enthält natürlich immer noch die gleichen Elemente, die sie immer hatte.

2
RemcoGerlich

Die Schleife hier ist irgendwie irrelevant. Ähnlich wie bei Funktionsparametern oder Argumenten ist das Einrichten einer solchen for-Schleife im Wesentlichen nur eine ausgefallene Zuweisung.

Ganzzahlen sind unveränderlich. Die einzige Möglichkeit, sie zu ändern, besteht darin, eine neue Ganzzahl zu erstellen und sie demselben Namen wie das Original zuzuweisen.

Pythons Semantik für die Zuweisung wird direkt auf Cs abgebildet (nicht überraschend angesichts der PyObject * -Zeiger von CPython). Die einzigen Einschränkungen sind, dass alles ein Zeiger ist und Sie keine doppelten Zeiger haben dürfen. Betrachten Sie den folgenden Code:

a = 1
b = a
b += 1
print(a)

Was geschieht? Es druckt 1. Warum? Es entspricht ungefähr dem folgenden C-Code:

i64* a = malloc(sizeof(i64));
*a = 1;
i64* b = a;
i64* tmp = malloc(sizeof(i64));
tmp = *b + 1;
b = tmp;
printf("%d\n", *a);

Im C-Code ist es offensichtlich, dass der Wert von a völlig unberührt bleibt.

Was den Grund betrifft, warum Listen zu funktionieren scheinen, lautet die Antwort im Grunde nur, dass Sie denselben Namen zuweisen. Listen sind veränderlich. Die Identität des Objekts mit dem Namen a[0] wird sich ändern, aber a[0] ist immer noch ein gültiger Name. Sie können dies mit dem folgenden Code überprüfen:

x = 1
a = [x]
print(a[0] is x)
a[0] += 1
print(a[0] is x)

Dies ist jedoch nichts Besonderes für Listen. Ersetzen Sie a[0] in diesem Code mit y und Sie erhalten genau das gleiche Ergebnis.

1
Kevin