Mir ist gerade aufgefallen, dass in Python, wenn man schreibt
for i in a:
i += 1
Die Elemente der ursprünglichen Liste a
sind eigentlich überhaupt nicht betroffen, da i
sich herausstellt, dass die Variable nur eine Kopie des ursprünglichen Elements in ist a
.
Um das ursprüngliche Element zu ändern,
for index, i in enumerate(a):
a[index] += 1
würde gebraucht werden.
Dieses Verhalten hat mich wirklich überrascht. 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 das Python-Tutorial schon einmal gelesen. Nur um sicher zu gehen, habe ich das Buch gerade noch einmal durchgesehen und es erwähnt dieses Verhalten überhaupt nicht.
Was ist die Begründung für dieses Design? Wird erwartet, dass es eine Standardübung in vielen Sprachen ist, sodass das Tutorial davon ausgeht, dass die Leser es auf natürliche Weise verstehen sollten? In welchen anderen Sprachen ist das gleiche Iterationsverhalten vorhanden, auf das ich in Zukunft achten sollte?
i
unveränderlich ist oder Sie eine nicht mutierende Operation ausführen. Bei einer verschachtelten Listefor i in a: a.append(1)
würde sich das Verhalten ändern. Python nicht die verschachtelten Listen kopieren. Ganzzahlen sind jedoch unveränderlich und Addition gibt ein neues Objekt zurück, es ändert das alte nicht.a=[1,2,3];a.forEach(i => i+=1);alert(a)
. Gleiche in C #i = i + 1
zu beeinflussena
?Antworten:
Ich habe kürzlich bereits eine ähnliche Frage beantwortet und es ist sehr wichtig zu wissen, dass
+=
dies unterschiedliche Bedeutungen haben kann:Wenn der Datentyp In-Place-Addition implementiert (dh eine ordnungsgemäß funktionierende
__iadd__
Funktion hat), werden die Daten,i
auf die verwiesen wird, aktualisiert (egal, ob sie in einer Liste oder an einem anderen Ort enthalten sind).Wenn der Datentyp keine
__iadd__
Methode implementiert , ist diei += x
Anweisung nur ein syntaktischer Zuckeri = i + x
. Daher wird ein neuer Wert erstellt und dem Variablennamen zugewieseni
.Wenn der Datentyp implementiert
__iadd__
, aber etwas Seltsames tut. Möglicherweise wird es aktualisiert ... oder nicht - das hängt davon ab, was dort implementiert ist.Python-Ganzzahlen, Floats und Strings werden nicht implementiert,
__iadd__
sodass sie nicht direkt aktualisiert werden. Andere Datentypen wienumpy.array
oderlist
implementieren es jedoch und verhalten sich so, wie Sie es erwartet haben. Es ist also keine Frage des Kopierens oder Nicht-Kopierens beim Iterieren (normalerweise werden keine Kopien fürlist
s undtuple
s erstellt - aber das hängt auch von der Implementierung der Container__iter__
und der__getitem__
Methode ab!) - es ist eher eine Frage des Datentyps Sie haben in Ihrem gespeicherta
.quelle
Klarstellung - Terminologie
Python unterscheidet nicht zwischen den Konzepten Referenz und Zeiger . Sie verwenden normalerweise nur den Begriff Referenz , aber wenn Sie mit Sprachen wie C ++ vergleichen, die diese Unterscheidung haben - es ist viel näher an einem Zeiger .
Da der Fragesteller eindeutig aus dem C ++ - Hintergrund stammt und diese Unterscheidung - die für die Erklärung erforderlich ist - in Python nicht vorhanden ist , habe ich mich für die Verwendung der C ++ - Terminologie entschieden:
void foo(int x);
ist eine Signatur einer Funktion, die eine Ganzzahl nach Wert empfängt .void foo(int* x);
ist eine Signatur einer Funktion, die eine Ganzzahl per Zeiger empfängt .void foo(int& x);
ist eine Signatur einer Funktion, die eine Ganzzahl als Referenz erhält .Was meinst du mit "anders als andere Sprachen"? Die meisten mir bekannten Sprachen, die 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 auf andere Sprachen mit ähnlichen architektonischen oder philosophischen Konzepten zutreffen können):
Dieses Verhalten kann bei Personen, die sich dessen nicht bewusst sind, zu Fehlern führen. Das alternative Verhalten kann jedoch auch bei Personen, die sich dessen bewusst sind, zu Fehlern führen . Wenn Sie eine Variable zuweisen (
i
), halten Sie normalerweise nicht an und berücksichtigen alle anderen Variablen, die aufgrund dieser Variable geändert würden (a
). Die Einschränkung des Bereichs, an dem Sie arbeiten, ist ein wichtiger Faktor für die Verhinderung von Spaghetti-Code. Daher ist die Iteration nach Kopie in der Regel die Standardeinstellung, auch in Sprachen, die die Iteration nach Referenz unterstützen.Python-Variablen sind immer einzelne Zeiger, daher ist es günstig, sie kopiert zu iterieren - günstiger als referenziert zu iterieren. Dies würde bei jedem Zugriff auf den Wert eine zusätzliche Verzögerung erfordern.
Python kennt keine Referenzvariablen wie zum Beispiel C ++. Das heißt, alle Variablen in Python sind Verweise, aber in dem Sinne, dass sie Zeiger sind - keine Verweise hinter den Kulissen wie C ++ -
type& name
Argumente. Da dieses Konzept in Python nicht vorhanden ist, sollten Sie die Iteration nach Referenz implementieren - geschweige denn zum Standard machen! - erfordert mehr Komplexität für den Bytecode.Pythons
for
Aussage funktioniert nicht nur bei Arrays, sondern auch bei einem allgemeineren Konzept von Generatoren. Hinter den Kulissen ruft Pythoniter
Ihre Arrays auf, um ein Objekt zu erhalten, das - wenn Sie es aufrufennext
- entweder das nächste Element oderraise
sa zurückgibtStopIteration
. 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.quelle
*it = ...
- aber diese Art von Syntax zeigt bereits an, dass Sie etwas an einem anderen Ort ändern -, was Grund Nr. 1 weniger problematisch macht. Die Gründe 2 und 3 treffen auch nicht zu, da das Kopieren in C ++ teuer ist und das Konzept der Referenzvariablen existiert. Was Grund 4 betrifft - die Möglichkeit, eine Referenz zurückzugeben, ermöglicht eine einfache Implementierung für alle Fälle.Keine der hier aufgeführten Antworten gibt Ihnen Code, mit dem Sie wirklich veranschaulichen können, warum dies im Python-Land passiert. Und es macht Spaß, dies in einem tieferen Sinne zu betrachten, und so geht es weiter.
Der Hauptgrund dafür, dass dies nicht wie erwartet funktioniert, ist, dass Sie in Python Folgendes schreiben:
es tut nicht das, was du denkst. Ganzzahlen sind unveränderlich. Dies kann man sehen, wenn man sich anschaut, was das Objekt tatsächlich in Python ist:
Die ID-Funktion repräsentiert einen eindeutigen und konstanten Wert für ein Objekt in seiner Lebensdauer. Konzeptionell ist es einer Speicheradresse in C / C ++ lose zugeordnet. Den obigen Code ausführen:
Dies bedeutet, dass die erste
a
nicht mehr mit der zweiten identisch ista
, da ihre IDs unterschiedlich sind. Tatsächlich befinden sie sich an verschiedenen Stellen im Speicher.Bei einem Objekt verhält es sich jedoch anders. Ich habe überschreibt die
+=
Betreiber hier:Das Ausführen dieses Befehls führt zu folgender Ausgabe:
Beachten Sie, dass das id-Attribut in diesem Fall für beide Iterationen identisch ist , auch wenn der Wert des Objekts unterschiedlich ist (Sie können auch
id
den int-Wert des Objekts ermitteln, der sich aufgrund von Ganzzahlen ändern würde, wenn er sich ändert) unveränderlich sind).Vergleichen Sie dies damit, wenn Sie dieselbe Übung mit einem unveränderlichen Objekt ausführen:
Dies gibt aus:
Ein paar Dinge hier zu beachten. Erstens
+=
fügen Sie in der Schleife mit dem nicht mehr zum ursprünglichen Objekt hinzu. In diesem Fall verwendet Python eine andere ID , da ints zu den unveränderlichen Typen in Python gehört. Interessant ist auch, dass Python denselben Basiswertid
für mehrere Variablen mit demselben unveränderlichen Wert verwendet:tl; dr - Python hat eine Handvoll unveränderlicher Typen, die das Verhalten verursachen, das Sie sehen. Für alle veränderlichen Typen ist Ihre Erwartung richtig.
quelle
@ Idans Antwort ist eine gute Erklärung dafür, warum Python die Schleifenvariable nicht wie in C als Zeiger behandelt, aber es lohnt sich, ausführlicher zu erklären, wie die Code-Snippets entpackt werden, wie in Python viele einfach erscheinende Bits Code wird tatsächlich Aufrufe von eingebauten Methoden sein . Um dein erstes Beispiel zu nehmen
Es gibt zwei Dinge zu entpacken: die
for _ in _:
Syntax und die_ += _
Syntax. Um die for-Schleife zuerst zu nehmen, hat Python wie andere Sprachen einefor-each
Schleife, die im Wesentlichen die Syntax sugar für ein Iteratormuster ist. In Python ist ein Iterator ein Objekt, das eine.__next__(self)
Methode definiert , die das aktuelle Element in der Sequenz zurückgibt, mit dem nächsten fortfährt und ein auslöst,StopIteration
wenn die Sequenz keine Elemente mehr enthält. Ein Iterable ist ein Objekt, das eine.__iter__(self)
Methode definiert , die einen Iterator zurückgibt.(Hinweis: ein
Iterator
ist auch einIterable
und kehrt von seiner.__iter__(self)
Methode zurück.)Python verfügt normalerweise über eine eingebaute Funktion, die an die benutzerdefinierte Methode mit doppeltem Unterstrich delegiert. Also hat es
iter(o)
was auflösto.__iter__()
undnext(o)
das auflösto.__next__()
. Beachten Sie, dass diese integrierten Funktionen häufig eine angemessene Standarddefinition verwenden, wenn die Methode, an die sie delegieren würden, nicht definiert ist. Zum Beispiellen(o)
löst in der Regel zu ,o.__len__()
aber wenn das Verfahren nicht definiert ist , wird es dann versucheniter(o).__len__()
.Ein for - Schleife ist im Wesentlichen in Bezug auf dem definiert
next()
,iter()
und basische Kontrollstrukturen. Im Allgemeinen der Codewerde zu sowas ausgepackt
Also in diesem Fall
wird ausgepackt
Die andere Hälfte davon ist
i += 1
. In der Regel%ASSIGN% += %EXPR%
wird ausgepackt%ASSIGN% = %ASSIGN%.__iadd__(%EXPR%)
. Hier__iadd__(self, other)
tut sich inplace add und kehrt zurück.(Hinweis: Dies ist ein weiterer Fall, in dem Python eine Alternative auswählt, wenn die Hauptmethode nicht definiert ist. Wenn das Objekt nicht implementiert wird
__iadd__
, wird auf diese zurückgegriffen__add__
. Dies geschieht in diesem Fall tatsächlich, daint
es nicht implementiert wird__iadd__
- was sinnvoll ist, weil sie sind unveränderlich und können daher nicht geändert werden.)Ihr Code hier sieht also so aus
wo wir definieren können
In Ihrem zweiten Teil des Codes ist noch ein bisschen mehr los. Die zwei neuen Dinge, die wir wissen müssen, sind die, in die
%ARG%[%KEY%] = %VALUE%
entpackt(%ARG%).__setitem__(%KEY%, %VALUE%)
und in die%ARG%[%KEY%]
entpackt wird(%ARG%).__getitem__(%KEY%)
. Wenn wir dieses Wissen zusammenstellen, werden wira[ix] += 1
entpackta.__setitem__(ix, a.__getitem__(ix).__add__(1))
(wieder:__add__
anstatt__iadd__
weil__iadd__
es nicht von ints implementiert wird). Unser endgültiger Code sieht folgendermaßen aus:Zur Beantwortung tatsächlich Ihre Frage, warum die erste ist die Liste nicht ändern , während der zweite Fall ist, in unserem ersten Schnipsel bekommen wir
i
ausnext(_a_iter)
, was bedeutet ,i
wird eine seinint
. Daint
's nicht an Ort und Stelle geändert werden kann,i += 1
ändert sich nichts an der Liste. In unserem zweiten Fall ändern wir wieder nichtint
die Liste, sondern ändern sie durch einen Aufruf__setitem__
.Der Grund für diese ganze aufwändige Übung ist, dass sie meiner Meinung nach die folgende Lektion über Python lehrt:
Die Methoden mit doppeltem Unterstrich sind eine Hürde für den Einstieg, aber für die Sicherung von Pythons Ruf als "lauffähiger Pseudocode" von entscheidender Bedeutung. Ein anständiger Python-Programmierer hat ein gründliches Verständnis für diese Methoden und wie sie aufgerufen werden, und definiert sie, wo immer dies sinnvoll ist.
Edit : @deltab korrigiert meine nachlässige Verwendung des Begriffs "Sammlung".
quelle
__len__
and__contains__
+=
Funktioniert unterschiedlich, je nachdem, ob der aktuelle Wert veränderlich oder unveränderlich ist . Dies war der Hauptgrund dafür, dass die Implementierung in Python lange auf sich warten ließ, da Python-Entwickler befürchteten, dass dies verwirrend sein könnte.Wenn
i
es sich um ein int handelt, kann es nicht geändert werden, da ints unveränderlich sind. Wenn sich daher der Wert voni
changes ändert, muss es notwendigerweise auf ein anderes Objekt verweisen:Wenn jedoch die linke Seite veränderlich ist , kann + = sie tatsächlich ändern. wie wenn es eine Liste ist:
i
Bezieht sich in Ihrer for-Schleife auf jedes Element dera
Reihe nach. Wenn dies ganze Zahlen sind, gilt der erste Fall, und das Ergebnis voni += 1
muss sein, dass es sich auf ein anderes ganzzahliges Objekt bezieht. Die Liste hata
natürlich immer noch die gleichen Elemente, die sie immer hatte.quelle
i = 1
Sätzei
zu einem unveränderlichen Integer - Objekt, danni = []
setzen sollteni
auf ein unveränderliche Liste Objekt. Mit anderen Worten, warum sind ganzzahlige Objekte unveränderlich und Listenobjekte veränderlich? Dahinter sehe ich keine Logik.list
implementiert Methoden, die ihren Inhalt ändern,int
nicht.[]
ist Objekt ein veränderliches Liste undi = []
kanni
auf dieses Objekt beziehen.+=
Operator / die Methode so implementiert , dass sie sich für beide Typen ähnlich verhält (Prinzip der geringsten Überraschung): entweder das ursprüngliche Objekt ändern oder eine geänderte Kopie für Ganzzahlen und Listen zurückgeben.+=
es in Python überraschend ist, aber es wurde der Eindruck erweckt , dass die anderen Optionen, die Sie erwähnen, auch überraschend oder zumindest weniger praktisch gewesen wären (das Ändern des ursprünglichen Objekts kann nicht mit dem gebräuchlichsten Werttyp durchgeführt werden) Sie verwenden + = mit, ints. Und das Kopieren einer ganzen Liste ist viel teurer als das Mutieren. Python kopiert keine Dinge wie Listen und Wörterbücher, es sei denn, dies wird ausdrücklich dazu aufgefordert. Es war damals eine große Debatte.Die Schleife hier ist irgendwie irrelevant. Ähnlich wie bei Funktionsparametern oder -argumenten ist das Einrichten einer for-Schleife im Wesentlichen nur eine ausgefallene Zuweisung.
Ganzzahlen sind unveränderlich. Sie können sie nur ändern, indem Sie eine neue Ganzzahl erstellen und sie demselben Namen wie das Original zuweisen.
Die Semantik von Python für die Zuweisung wird direkt auf Cs abgebildet (nicht überraschend bei CPythons PyObject * -Pointern). Die einzige Einschränkung besteht darin, dass alles ein Zeiger ist und Sie keine doppelten Zeiger haben dürfen. Betrachten Sie den folgenden Code:
Was geschieht? Es druckt
1
. Warum? Es entspricht ungefähr dem folgenden C-Code:Im C-Code ist es offensichtlich, dass der Wert von überhaupt nicht
a
betroffen ist.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 benannten Objekts
a[0]
ändert sich, esa[0]
handelt sich jedoch weiterhin um einen gültigen Namen. Sie können dies mit dem folgenden Code überprüfen:Dies ist jedoch nichts Besonderes für Listen. Ersetzen Sie
a[0]
in diesem Code mity
und Sie erhalten genau das gleiche Ergebnis.quelle