Warum verhält sich + = auf Listen unerwartet?

118

Der +=Operator in Python scheint unerwartet mit Listen zu arbeiten. Kann mir jemand sagen, was hier los ist?

class foo:  
     bar = []
     def __init__(self,x):
         self.bar += [x]


class foo2:
     bar = []
     def __init__(self,x):
          self.bar = self.bar + [x]

f = foo(1)
g = foo(2)
print f.bar
print g.bar 

f.bar += [3]
print f.bar
print g.bar

f.bar = f.bar + [4]
print f.bar
print g.bar

f = foo2(1)
g = foo2(2)
print f.bar 
print g.bar 

AUSGABE

[1, 2]
[1, 2]
[1, 2, 3]
[1, 2, 3]
[1, 2, 3, 4]
[1, 2, 3]
[1]
[2]

foo += bar scheint jede Instanz der Klasse zu betreffen, während foo = foo + bar so zu verhalten scheint, wie ich es erwarten würde.

Der +=Operator wird als "zusammengesetzter Zuweisungsoperator" bezeichnet.

Eukalkulie
quelle
Sehen Sie auch den Unterschied zwischen 'Erweitern' und 'Anhängen' auf der Liste
N 1.1
3
Ich glaube nicht, dass dies mit Python nicht stimmt. In den meisten Sprachen können Sie den +Operator nicht einmal für Arrays verwenden. Ich denke, dass es in diesem Fall durchaus Sinn macht, +=das anzuhängen.
Skilldrick
4
Es heißt offiziell "Augmented Assignment".
Martijn Pieters

Antworten:

137

Die allgemeine Antwort lautet, dass +=versucht wird, die __iadd__spezielle Methode aufzurufen , und wenn diese nicht verfügbar ist, wird versucht, sie zu verwenden__add__ stattdessen . Das Problem ist also der Unterschied zwischen diesen speziellen Methoden.

Die __iadd__spezielle Methode ist für eine In-Place-Addition vorgesehen, dh sie mutiert das Objekt, auf das sie einwirkt. Die __add__spezielle Methode gibt ein neues Objekt zurück und wird auch für den Standardoperator verwendet +.

Wenn der +=Operator für ein Objekt verwendet wird, für das eine __iadd__Definition vorliegt, wird das Objekt an Ort und Stelle geändert. Andernfalls wird stattdessen versucht, die Ebene zu verwenden __add__und ein neues Objekt zurückzugeben.

Aus diesem Grund +=ändert für veränderbare Typen wie Listen der Wert des Objekts, während für unveränderliche Typen wie Tupel, Zeichenfolgen und Ganzzahlen stattdessen ein neues Objekt zurückgegeben wird ( a += bentsprichta = a + b ).

Für die Typen , dass die Unterstützung sowohl __iadd__und __add__Sie deshalb sein müssen aufpassen , welche Sie verwenden. a += bruft auf __iadd__und mutiert a, während a = a + bein neues Objekt erstellt und zugewiesen wird a. Sie sind nicht die gleiche Operation!

>>> a1 = a2 = [1, 2]
>>> b1 = b2 = [1, 2]
>>> a1 += [3]          # Uses __iadd__, modifies a1 in-place
>>> b1 = b1 + [3]      # Uses __add__, creates new list, assigns it to b1
>>> a2
[1, 2, 3]              # a1 and a2 are still the same list
>>> b2
[1, 2]                 # whereas only b1 was changed

Für unveränderliche Typen (wo Sie keine haben __iadd__) a += bund a = a + bgleichwertig sind. Dies ist es, was Sie +=für unveränderliche Typen verwenden können, was eine seltsame Entwurfsentscheidung sein könnte, bis Sie bedenken, dass Sie es sonst nicht +=für unveränderliche Typen wie Zahlen verwenden könnten !

Scott Griffiths
quelle
4
Es gibt auch __radd__Methoden, die manchmal aufgerufen werden können (sie sind relevant für Ausdrücke, die hauptsächlich Unterklassen betreffen).
JFS
2
In der Perspektive: + = ist nützlich, wenn Speicher und Geschwindigkeit wichtig sind
Norfeldt
3
In dem Wissen, dass eine Liste +=tatsächlich erweitert wird , erklärt dies, warum x = []; x = x + {}eine TypeErrorWeile x = []; x += {}nur zurückkehrt [].
Zezollo
95

Für den allgemeinen Fall siehe die Antwort von Scott Griffith . Beim Umgang mit Listen wie Ihnen ist der +=Operator jedoch eine Abkürzung für someListObject.extend(iterableObject). Siehe die Dokumentation von extens () .

Das extend Funktion hängt alle Elemente des Parameters an die Liste an.

Wenn Sie dies tun, ändern foo += somethingSie die Liste fooan Ort und Stelle, sodass Sie nicht die Referenz ändern, auf die der Name fooverweist, sondern das Listenobjekt direkt ändern. Mit foo = foo + somethingerstellen Sie tatsächlich eine neue Liste.

Dieser Beispielcode erklärt es:

>>> l = []
>>> id(l)
13043192
>>> l += [3]
>>> id(l)
13043192
>>> l = l + [3]
>>> id(l)
13059216

Beachten Sie, wie sich die Referenz ändert, wenn Sie die neue Liste neu zuweisen l .

Da bares sich um eine Klassenvariable anstelle einer Instanzvariablen handelt, wirkt sich eine Änderung an Ort und Stelle auf alle Instanzen dieser Klasse aus. Bei der Neudefinition self.barverfügt die Instanz jedoch über eine separate Instanzvariable, self.barohne die anderen Klasseninstanzen zu beeinflussen.

AndiDog
quelle
7
Dies ist nicht immer wahr: a = 1; a + = 1; ist gültiges Python, aber Ints haben keine "extens ()" - Methoden. Sie können dies nicht verallgemeinern.
E-Satis
2
Scott Griffiths hat einige Tests durchgeführt und alles richtig gemacht, also -1 für Sie.
E-Satis
11
@ e-statis: Das OP sprach eindeutig über Listen, und ich habe klar gesagt, dass ich auch über Listen spreche. Ich verallgemeinere nichts.
AndiDog
-1 entfernt, die Antwort ist gut genug. Ich denke immer noch, dass Griffiths Antwort besser ist.
E-Satis
Zuerst fühlt es sich komisch an zu denken, dass a += bsich das von a = a + bzwei Listen unterscheidet aund b. Aber es macht Sinn; extendDies ist häufiger beabsichtigt, um mit Listen zu tun, als eine neue Kopie der gesamten Liste zu erstellen, die eine höhere zeitliche Komplexität aufweist. Wenn Entwickler darauf achten müssen, dass sie die ursprünglichen Listen nicht ändern, sind Tupel eine bessere Option, da sie unveränderliche Objekte sind. +=mit Tupeln kann das ursprüngliche Tupel nicht ändern.
Pranjal Mittal
22

Das Problem hierbei ist, dass bares als Klassenattribut definiert ist, nicht als Instanzvariable.

In foowird das Klassenattribut in der initMethode geändert , weshalb alle Instanzen betroffen sind.

In foo2wird eine Instanzvariable mithilfe des (leeren) Klassenattributs definiert, und jede Instanz erhält ihre eigene bar.

Die "richtige" Implementierung wäre:

class foo:
    def __init__(self, x):
        self.bar = [x]

Natürlich sind Klassenattribute völlig legal. Tatsächlich können Sie auf sie zugreifen und sie ändern, ohne eine Instanz der Klasse wie folgt zu erstellen:

class foo:
    bar = []

foo.bar = [x]
Kann Berk Güder
quelle
8

Hier geht es um zwei Dinge:

1. class attributes and instance attributes
2. difference between the operators + and += for lists

+Der Operator ruft die __add__Methode in einer Liste auf. Es nimmt alle Elemente aus seinen Operanden und erstellt eine neue Liste mit den Elementen, die ihre Reihenfolge beibehalten.

+= Bedieneraufrufe __iadd__ Methode in der Liste auf. Es nimmt eine Iterable und hängt alle Elemente der Iterable an die vorhandene Liste an. Es wird kein neues Listenobjekt erstellt.

In der Klasse ist foodie Anweisung self.bar += [x]keine Zuweisungsanweisung, sondern übersetzt tatsächlich in

self.bar.__iadd__([x])  # modifies the class attribute  

Dadurch wird die Liste an Ort und Stelle geändert und es wird die Listenmethode verwendet extend.

In der Klasse foo2dagegen die Zuweisungsanweisung in der initMethode

self.bar = self.bar + [x]  

kann wie folgt dekonstruiert werden:
Die Instanz hat kein Attribut bar(es gibt jedoch ein gleichnamiges Klassenattribut), greift also auf das Klassenattribut zu barund erstellt eine neue Liste, indem sie xan dieses angehängt wird. Die Aussage übersetzt zu:

self.bar = self.bar.__add__([x]) # bar on the lhs is the class attribute 

Anschließend wird ein Instanzattribut erstellt barund die neu erstellte Liste zugewiesen. Beachten Sie, dass sich bardie Rhs der Zuordnung von der unterscheidetbar auf den LHS unterscheiden.

Für Instanzen der Klasse foo, barist ein Klassenattribut und nicht die Instanz - Attribut. Daher wird jede Änderung des Klassenattributs barfür alle Instanzen berücksichtigt.

Im Gegenteil, jede Instanz der Klasse foo2hat ein eigenes Instanzattribut, bardas sich vom gleichnamigen Klassenattribut unterscheidet bar.

f = foo2(4)
print f.bar # accessing the instance attribute. prints [4]  
print f.__class__.bar # accessing the class attribute. prints []  

Hoffe das klärt die Dinge.

ajay
quelle
5

Obwohl viel Zeit vergangen ist und viele richtige Dinge gesagt wurden, gibt es keine Antwort, die beide Effekte bündelt.

Sie haben 2 Effekte:

  1. ein "spezielles", möglicherweise unbemerktes Verhalten von Listen mit +=(wie von Scott Griffiths angegeben )
  2. die Tatsache, dass sowohl Klassenattribute als auch Instanzattribute beteiligt sind (wie von Can Berk Büder angegeben )

In der Klasse foo, die __init__modifiziert Methode , um das Klassenattribut. Es ist, weil self.bar += [x]übersetzt zu self.bar = self.bar.__iadd__([x]). __iadd__()dient der Inplace-Änderung, ändert also die Liste und gibt einen Verweis darauf zurück.

Beachten Sie, dass das Instanz-Diktat geändert wird, obwohl dies normalerweise nicht erforderlich wäre, da das Klassendiktat bereits dieselbe Zuweisung enthält. Dieses Detail bleibt also fast unbemerkt - außer wenn Sie es foo.bar = []später tun . Hier bleiben die Instanzen bardank dieser Tatsache gleich.

In der Klasse foo2wird die Klasse barjedoch verwendet, aber nicht berührt. Stattdessen wird a [x]hinzugefügt, um ein neues Objekt zu bilden, wie self.bar.__add__([x])hier genannt, das das Objekt nicht ändert. Das Ergebnis wird dann in das Instanz-Diktat eingefügt und gibt der Instanz die neue Liste als Diktat, während das Attribut der Klasse geändert bleibt.

Die Unterscheidung zwischen ... = ... + ...und ... += ...wirkt sich auch auf die nachfolgenden Aufgaben aus:

f = foo(1) # adds 1 to the class's bar and assigns f.bar to this as well.
g = foo(2) # adds 2 to the class's bar and assigns g.bar to this as well.
# Here, foo.bar, f.bar and g.bar refer to the same object.
print f.bar # [1, 2]
print g.bar # [1, 2]

f.bar += [3] # adds 3 to this object
print f.bar # As these still refer to the same object,
print g.bar # the output is the same.

f.bar = f.bar + [4] # Construct a new list with the values of the old ones, 4 appended.
print f.bar # Print the new one
print g.bar # Print the old one.

f = foo2(1) # Here a new list is created on every call.
g = foo2(2)
print f.bar # So these all obly have one element.
print g.bar 

Sie können die Identität der Objekte mit überprüfen print id(foo), id(f), id(g)(vergessen Sie nicht die zusätzlichen() s, wenn Sie mit Python3 arbeiten).

Übrigens: Der +=Bediener wird als "erweiterte Zuweisung" bezeichnet und soll im Allgemeinen so weit wie möglich Änderungen an Ort und Stelle vornehmen.

glglgl
quelle
5

Die anderen Antworten scheinen es so ziemlich abgedeckt zu haben, obwohl es sich lohnt, die Augmented Assignments PEP 203 zu zitieren und darauf zu verweisen :

Sie [die erweiterten Zuweisungsoperatoren] implementieren denselben Operator wie ihre normale Binärform, außer dass die Operation "an Ort und Stelle" ausgeführt wird, wenn das Objekt auf der linken Seite dies unterstützt, und dass die linke Seite nur einmal ausgewertet wird.

...

Die Idee hinter der erweiterten Zuweisung in Python ist, dass es nicht nur eine einfachere Möglichkeit ist, die übliche Praxis des Speicherns des Ergebnisses einer binären Operation in ihrem linken Operanden zu schreiben, sondern auch eine Möglichkeit für den fraglichen linken Operanden wissen, dass es "auf sich selbst" arbeiten sollte, anstatt eine modifizierte Kopie von sich selbst zu erstellen.

mwardm
quelle
1
>>> elements=[[1],[2],[3]]
>>> subset=[]
>>> subset+=elements[0:1]
>>> subset
[[1]]
>>> elements
[[1], [2], [3]]
>>> subset[0][0]='change'
>>> elements
[['change'], [2], [3]]

>>> a=[1,2,3,4]
>>> b=a
>>> a+=[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4, 5])
>>> a=[1,2,3,4]
>>> b=a
>>> a=a+[5]
>>> a,b
([1, 2, 3, 4, 5], [1, 2, 3, 4])
Tanglei
quelle
0
>>> a = 89
>>> id(a)
4434330504
>>> a = 89 + 1
>>> print(a)
90
>>> id(a)
4430689552  # this is different from before!

>>> test = [1, 2, 3]
>>> id(test)
48638344L
>>> test2 = test
>>> id(test)
48638344L
>>> test2 += [4]
>>> id(test)
48638344L
>>> print(test, test2)  # [1, 2, 3, 4] [1, 2, 3, 4]```
([1, 2, 3, 4], [1, 2, 3, 4])
>>> id(test2)
48638344L # ID is different here

Wir sehen, dass Python uns beim Versuch, ein unveränderliches Objekt (in diesem Fall eine Ganzzahl) zu ändern, stattdessen einfach ein anderes Objekt gibt. Auf der anderen Seite können wir Änderungen an einem veränderlichen Objekt (einer Liste) vornehmen und dafür sorgen, dass es durchgehend dasselbe Objekt bleibt.

Ref: https://medium.com/@tyastropheus/tricky-python-i-memory-management-for-mutable-immutable-objects-21507d1e5b95

Lesen Sie auch die unten stehende URL, um die Flach- und Tiefenkopie zu verstehen

https://www.geeksforgeeks.org/copy-python-deep-copy-shallow-copy/

Roshan ok
quelle
# ID ist das gleiche für Listen
Roshan ok