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?

MarJamRob
quelle
7
+=verhält sich wie extend()bei Listen.
Ashwini Chaudhary
12
@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. :)
Kojiro
1
Verwandte Frage zu den Unterschieden zwischen den beiden in Java: stackoverflow.com/a/7456548/245966
jakub.g

Antworten:

317

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. 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 NotImplemented und 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

foo_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 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.

mgilson
quelle
18
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)
except AttributeError:
    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.)

abarnert
quelle
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)- 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.
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]
Deqing
quelle