Wann unterscheidet sich "i + = x" von "i = i + x" in Python?
212
Mir wurde gesagt, dass +=dies andere Auswirkungen haben kann als die Standardnotation von i = i +. Gibt es einen Fall, in dem i += 1es anders wäre als i = i + 1?
@AshwiniChaudhary Das ist ein ziemlich feiner Unterschied, wenn man bedenkt , dass i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6]ist True. Viele Entwickler bemerken diese id(i)Änderungen möglicherweise nicht für einen Vorgang, nicht jedoch für den anderen.
Kojiro
1
@kojiro - Obwohl es eine subtile Unterscheidung ist, denke ich, dass es eine wichtige ist.
mgilson
@mgilson es ist wichtig, und so fand ich, dass es einer Erklärung bedarf. :)
Aus API-Sicht __iadd__soll es verwendet werden, um veränderbare Objekte an Ort und Stelle zu ändern (das mutierte Objekt zurückzugeben), während __add__eine neue Instanz von etwas zurückgegeben werden soll. Bei unveränderlichen Objekten geben beide Methoden eine neue Instanz zurück, __iadd__setzen die neue Instanz jedoch in den aktuellen Namespace mit demselben Namen wie die alte Instanz. Deshalb
i =1
i +=1
scheint zu erhöhen i. In Wirklichkeit erhalten Sie eine neue Ganzzahl und weisen sie "über" zu i- wobei ein Verweis auf die alte Ganzzahl verloren geht. In diesem Fall i += 1ist genau das gleiche wie i = i + 1. Bei den meisten veränderlichen Objekten ist dies jedoch eine andere Geschichte:
Als konkretes Beispiel:
a =[1,2,3]
b = a
b +=[1,2,3]print a #[1, 2, 3, 1, 2, 3]print b #[1, 2, 3, 1, 2, 3]
verglichen mit:
a =[1,2,3]
b = a
b = b +[1,2,3]print a #[1, 2, 3]print b #[1, 2, 3, 1, 2, 3]
Beachten Sie, wie im ersten Beispiel, da bund adas gleiche Objekt verweisen, wenn ich +=auf b, es ändert sich tatsächlich b(und asieht , dass der Wandel zu - Schließlich ist es die gleiche Liste ist Referenzierung). Im zweiten Fall b = b + [1, 2, 3]nimmt dies jedoch die Liste, auf bdie verwiesen wird, und verkettet sie mit einer neuen Liste [1, 2, 3]. Anschließend wird die verkettete Liste im aktuellen Namespace wie folgt gespeichert: b- Ohne Rücksicht auf die vorherige bZeile.
1 In dem Ausdruck x + y, wenn x.__add__nicht implementiert ist oder wenn x.__add__(y)Erträge NotImplementedund xund yverschiedene Typen haben , dann x + yversucht Anruf y.__radd__(x). Also, in dem Fall, wo Sie haben
foo_instance += bar_instance
Wenn Foonicht implementiert __add__oder __iadd__dann ist das Ergebnis hier das gleiche wie
2 In dem Ausdruck foo_instance + bar_instance, bar_instance.__radd__wird versucht werden , bevor , foo_instance.__add__wenn die Art des bar_instanceeine Unterklasse von der Art der ist foo_instance(zB issubclass(Bar, Foo)). Der Grund für dies ist , weil Barin einem gewissen Sinne ist ein „höhere Ebene“ Objekt als Fooso Barsollte die Möglichkeit erhalten , zu überschreiben Foo‚s Verhalten.
Nun, +=ruft , __iadd__wenn es vorhanden ist , und fällt zurück auf das Hinzufügen und rebinding anders. Deshalb i = 1; i += 1funktioniert es, obwohl es keine gibt int.__iadd__. Aber abgesehen von dieser kleinen Schwäche, großartige Erklärungen.
abarnert
4
@abarnert - Ich habe immer angenommen, dass int.__iadd__gerade angerufen __add__. Ich bin froh, heute etwas Neues gelernt zu haben :).
mgilson
@abarnert - Ich nehme an, um vollständig zu sein , x + yruft an, y.__radd__(x)wenn x.__add__es nicht existiert (oder kehrt zurück NotImplementedund xund ysind von verschiedenen Typen)
mgilson
Wenn Sie wirklich vollständig sein möchten, müssen Sie erwähnen, dass das Bit "Wenn es existiert" die üblichen getattr-Mechanismen durchläuft, mit Ausnahme einiger Macken mit klassischen Klassen, und für Typen, die in der C-API implementiert sind, stattdessen nach beiden gesucht wird nb_inplace_addoder sq_inplace_concat, und diese C-API-Funktionen stellen strengere Anforderungen als die Python-Dunder-Methoden und… Aber ich denke nicht, dass dies für die Antwort relevant ist. Der Hauptunterschied besteht darin, dass +=versucht wird, ein In-Place-Add durchzuführen, bevor auf das Verhalten zurückgegriffen wird, wie +Sie es wahrscheinlich bereits erklärt haben.
abarnert
Ja, ich nehme an, Sie haben Recht ... Obwohl ich einfach auf die Haltung zurückgreifen könnte, dass die C-API nicht Teil von Python ist . Es ist Teil von Cpython :-P
mgilson
67
Macht unter der Decke i += 1so etwas:
try:
i = i.__iadd__(1)exceptAttributeError:
i = i.__add__(1)
Während i = i + 1macht so etwas:
i = i.__add__(1)
Dies ist eine leichte Vereinfachung, aber Sie haben die Idee: Python bietet Typen eine Möglichkeit, +=speziell damit umzugehen, indem sie sowohl eine __iadd__Methode als auch eine erstellen __add__.
Die Absicht ist, dass sich veränderbare Typen wie listmutieren __iadd__(und dann zurückkehren self, es sei denn, Sie tun etwas sehr Kniffliges), während unveränderliche Typen wie intes einfach nicht implementieren.
Beispielsweise:
>>> l1 =[]>>> l2 = l1
>>> l1 +=[3]>>> l2
[3]
Weil l2es dasselbe Objekt ist wie l1und Sie mutiert haben l1, haben Sie auch mutiert l2.
Aber:
>>> l1 =[]>>> l2 = l1
>>> l1 = l1 +[3]>>> l2
[]
Hier hast du nicht mutiert l1; Stattdessen haben Sie eine neue Liste erstellt l1 + [3]und den Namen neu l1gebunden, um l2darauf zu zeigen, wobei Sie auf die ursprüngliche Liste zeigen.
(In der +=Version haben Sie auch neu gebunden. l1In diesem Fall haben Sie es nur an das listgebunden, an das es bereits gebunden war, sodass Sie diesen Teil normalerweise ignorieren können.)
ruft __iadd__eigentlich __add__im Falle eines AttributeError?
mgilson
Nun, i.__iadd__ruft nicht an __add__; es ist i += 1das ruft __add__.
abarnert
ähm ... Ja, das habe ich gemeint. Interessant. Ich wusste nicht, dass das automatisch gemacht wurde.
mgilson
3
Der erste Versuch ist tatsächlich i = i.__iadd__(1)- iaddkann das Objekt an Ort und Stelle ändern, muss es aber nicht und wird daher in beiden Fällen voraussichtlich das Ergebnis zurückgeben.
lvc
Beachten Sie, dass dies bedeutet , dass operator.iaddAnrufe __add__auf AttributeError, aber es ist nicht das Ergebnis erneut binden kann ... so i=1; operator.iadd(i, 1)kehrt 2 und Blätter iauf 1. Welches ist ein bisschen verwirrend.
abarnert
6
Hier ist ein Beispiel , das direkt vergleicht i += xmit i = i + x:
def foo(x):
x = x +[42]def bar(x):
x +=[42]
c =[27]
foo(c);# c is not changed
bar(c);# c is changed to [27, 42]
+=
verhält sich wieextend()
bei Listen.i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6]
istTrue
. Viele Entwickler bemerken dieseid(i)
Änderungen möglicherweise nicht für einen Vorgang, nicht jedoch für den anderen.Antworten:
Dies hängt ganz vom Objekt ab
i
.+=
ruft die__iadd__
Methode auf (falls vorhanden) -__add__
greift zurück, wenn sie nicht vorhanden ist), während+
die__add__
Methode 1 oder__radd__
in einigen Fällen die Methode 2 aufgerufen wird .Aus API-Sicht
__iadd__
soll es verwendet werden, um veränderbare Objekte an Ort und Stelle zu ändern (das mutierte Objekt zurückzugeben), während__add__
eine neue Instanz von etwas zurückgegeben werden soll. Bei unveränderlichen Objekten geben beide Methoden eine neue Instanz zurück,__iadd__
setzen die neue Instanz jedoch in den aktuellen Namespace mit demselben Namen wie die alte Instanz. Deshalbscheint zu erhöhen
i
. In Wirklichkeit erhalten Sie eine neue Ganzzahl und weisen sie "über" zui
- wobei ein Verweis auf die alte Ganzzahl verloren geht. In diesem Falli += 1
ist genau das gleiche wiei = i + 1
. Bei den meisten veränderlichen Objekten ist dies jedoch eine andere Geschichte:Als konkretes Beispiel:
verglichen mit:
Beachten Sie, wie im ersten Beispiel, da
b
unda
das gleiche Objekt verweisen, wenn ich+=
aufb
, es ändert sich tatsächlichb
(unda
sieht , dass der Wandel zu - Schließlich ist es die gleiche Liste ist Referenzierung). Im zweiten Fallb = b + [1, 2, 3]
nimmt dies jedoch die Liste, aufb
die verwiesen wird, und verkettet sie mit einer neuen Liste[1, 2, 3]
. Anschließend wird die verkettete Liste im aktuellen Namespace wie folgt gespeichert:b
- Ohne Rücksicht auf die vorherigeb
Zeile.1 In dem Ausdruck
x + y
, wennx.__add__
nicht implementiert ist oder wennx.__add__(y)
ErträgeNotImplemented
undx
undy
verschiedene Typen haben , dannx + y
versucht Anrufy.__radd__(x)
. Also, in dem Fall, wo Sie habenfoo_instance += bar_instance
Wenn
Foo
nicht implementiert__add__
oder__iadd__
dann ist das Ergebnis hier das gleiche wiefoo_instance = bar_instance.__radd__(bar_instance, foo_instance)
2 In dem Ausdruck
foo_instance + bar_instance
,bar_instance.__radd__
wird versucht werden , bevor ,foo_instance.__add__
wenn die Art desbar_instance
eine Unterklasse von der Art der istfoo_instance
(zBissubclass(Bar, Foo)
). Der Grund für dies ist , weilBar
in einem gewissen Sinne ist ein „höhere Ebene“ Objekt alsFoo
soBar
sollte die Möglichkeit erhalten , zu überschreibenFoo
‚s Verhalten.quelle
+=
ruft ,__iadd__
wenn es vorhanden ist , und fällt zurück auf das Hinzufügen und rebinding anders. Deshalbi = 1; i += 1
funktioniert es, obwohl es keine gibtint.__iadd__
. Aber abgesehen von dieser kleinen Schwäche, großartige Erklärungen.int.__iadd__
gerade angerufen__add__
. Ich bin froh, heute etwas Neues gelernt zu haben :).x + y
ruft an,y.__radd__(x)
wennx.__add__
es nicht existiert (oder kehrt zurückNotImplemented
undx
undy
sind von verschiedenen Typen)nb_inplace_add
odersq_inplace_concat
, und diese C-API-Funktionen stellen strengere Anforderungen als die Python-Dunder-Methoden und… Aber ich denke nicht, dass dies für die Antwort relevant ist. Der Hauptunterschied besteht darin, dass+=
versucht wird, ein In-Place-Add durchzuführen, bevor auf das Verhalten zurückgegriffen wird, wie+
Sie es wahrscheinlich bereits erklärt haben.Macht unter der Decke
i += 1
so etwas:Während
i = i + 1
macht so etwas:Dies ist eine leichte Vereinfachung, aber Sie haben die Idee: Python bietet Typen eine Möglichkeit,
+=
speziell damit umzugehen, indem sie sowohl eine__iadd__
Methode als auch eine erstellen__add__
.Die Absicht ist, dass sich veränderbare Typen wie
list
mutieren__iadd__
(und dann zurückkehrenself
, es sei denn, Sie tun etwas sehr Kniffliges), während unveränderliche Typen wieint
es einfach nicht implementieren.Beispielsweise:
Weil
l2
es dasselbe Objekt ist wiel1
und Sie mutiert habenl1
, haben Sie auch mutiertl2
.Aber:
Hier hast du nicht mutiert
l1
; Stattdessen haben Sie eine neue Liste erstelltl1 + [3]
und den Namen neul1
gebunden, uml2
darauf zu zeigen, wobei Sie auf die ursprüngliche Liste zeigen.(In der
+=
Version haben Sie auch neu gebunden.l1
In diesem Fall haben Sie es nur an daslist
gebunden, an das es bereits gebunden war, sodass Sie diesen Teil normalerweise ignorieren können.)quelle
__iadd__
eigentlich__add__
im Falle einesAttributeError
?i.__iadd__
ruft nicht an__add__
; es isti += 1
das ruft__add__
.i = i.__iadd__(1)
-iadd
kann das Objekt an Ort und Stelle ändern, muss es aber nicht und wird daher in beiden Fällen voraussichtlich das Ergebnis zurückgeben.operator.iadd
Anrufe__add__
aufAttributeError
, aber es ist nicht das Ergebnis erneut binden kann ... soi=1; operator.iadd(i, 1)
kehrt 2 und Blätteri
auf1
. Welches ist ein bisschen verwirrend.Hier ist ein Beispiel , das direkt vergleicht
i += x
miti = i + x
:quelle