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?


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.


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


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]
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.