Perché + = si comporta in modo imprevisto nelle liste?


118

L' +=operatore in Python sembra funzionare in modo imprevisto sugli elenchi. Qualcuno può dirmi cosa sta succedendo qui?

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 

PRODUZIONE

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

foo += barsembra influenzare ogni istanza della classe, mentre foo = foo + barsembra comportarsi nel modo in cui mi aspetto che le cose si comportino.

L' +=operatore è chiamato "operatore di assegnazione composto".


vedi anche la differenza tra "estendi" e "aggiungi" sulla lista
N 1.1

3
Non credo che questo mostri qualcosa di sbagliato in Python. La maggior parte delle lingue non ti consentirebbe nemmeno di utilizzare l' +operatore sugli array. Penso che in questo caso abbia perfettamente senso +=aggiungere.
Skilldrick

4
Si chiama ufficialmente "assegnazione aumentata".
Martijn Pieters

Risposte:


138

La risposta generale è che +=prova a chiamare il __iadd__metodo speciale e, se non è disponibile, prova a usarlo __add__. Quindi il problema è con la differenza tra questi metodi speciali.

Il __iadd__metodo speciale è per un'aggiunta sul posto, cioè muta l'oggetto su cui agisce. Il __add__metodo speciale restituisce un nuovo oggetto ed è utilizzato anche per l' +operatore standard .

Quindi, quando l' +=operatore viene utilizzato su un oggetto che ha una __iadd__definizione, l'oggetto viene modificato in posizione. Altrimenti proverà invece a usare il semplice __add__e restituirà un nuovo oggetto.

Questo è il motivo per cui per i tipi modificabili come le liste +=cambia il valore dell'oggetto, mentre per i tipi immutabili come le tuple, le stringhe e gli interi viene invece restituito un nuovo oggetto ( a += bdiventa equivalente a a = a + b).

Per i tipi che supportano entrambi __iadd__e __add__quindi devi stare attento a quale usi. a += bchiamerà __iadd__e muterà a, mentre a = a + bcreerà un nuovo oggetto e lo assegnerà a a. Non sono la stessa operazione!

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

Per i tipi immutabili (dove non hai un __iadd__) a += be a = a + bsono equivalenti. Questo è ciò che ti consente di utilizzare +=su tipi immutabili, che potrebbe sembrare una strana decisione di progettazione finché non consideri che altrimenti non potresti usare +=su tipi immutabili come i numeri!


4
C'è anche un __radd__metodo che a volte può essere chiamato (è rilevante per le espressioni che coinvolgono principalmente sottoclassi).
jfs

2
In prospettiva: + = è utile se la memoria e la velocità sono importanti
Norfeldt

3
Sapendo che in +=realtà si estende un elenco, questo spiega perché x = []; x = x + {}un TypeErrorpo 'di tempo x = []; x += {}ritorna [].
zezollo

96

Per il caso generale, vedere la risposta di Scott Griffith . Quando si tratta di elenchi come te, tuttavia, l' +=operatore è una scorciatoia per someListObject.extend(iterableObject). Vedi la documentazione di extent () .

La extendfunzione aggiungerà tutti gli elementi del parametro all'elenco.

Quando lo fai foo += something, stai modificando l'elenco fooin posizione, quindi non cambi il riferimento a cui foopunta il nome , ma stai cambiando direttamente l'oggetto elenco. Con foo = foo + something, stai effettivamente creando un nuovo elenco.

Questo codice di esempio lo spiegherà:

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

Notare come cambia il riferimento quando si riassegna il nuovo elenco a l.

Poiché barè una variabile di classe invece di una variabile di istanza, la modifica sul posto interesserà tutte le istanze di quella classe. Ma durante la ridefinizione self.bar, l'istanza avrà una variabile di istanza separata self.barsenza influire sulle altre istanze di classe.


7
Questo non è sempre vero: a = 1; a + = 1; è Python valido, ma gli int non hanno alcun metodo "extent ()". Non puoi generalizzare questo.
e-satis

2
Fatto alcuni test, Scott Griffiths ha capito bene, quindi -1 per te.
e-satis

11
@ e-statis: L'OP stava chiaramente parlando di elenchi, e ho affermato chiaramente che sto parlando anche di elenchi. Non sto generalizzando nulla.
AndiDog

Tolto -1, la risposta è abbastanza buona. Penso ancora che la risposta di Griffiths sia migliore.
e-satis

All'inizio sembra strano pensare che a += bsia diverso da a = a + bper due elenchi ae b. Ma ha senso; extendsarebbe più spesso la cosa intesa da fare con gli elenchi piuttosto che creare una nuova copia dell'intero elenco che avrà una maggiore complessità temporale. Se gli sviluppatori devono fare attenzione a non modificare gli elenchi originali in posizione, le tuple sono un'opzione migliore essendo oggetti immutabili. +=con le tuple non è possibile modificare la tupla originale.
Pranjal Mittal

22

Il problema qui è che barè definito come un attributo di classe, non una variabile di istanza.

In foo, l'attributo class viene modificato nel initmetodo, ecco perché tutte le istanze sono interessate.

In foo2, una variabile di istanza viene definita utilizzando l'attributo di classe (vuoto) e ogni istanza ottiene il proprio bar.

L'implementazione "corretta" sarebbe:

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

Ovviamente, gli attributi di classe sono completamente legali. In effetti, puoi accedervi e modificarli senza creare un'istanza della classe come questa:

class foo:
    bar = []

foo.bar = [x]

8

Ci sono due cose coinvolte qui:

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

+operatore chiama il __add__metodo su un elenco. Prende tutti gli elementi dai suoi operandi e crea un nuovo elenco contenente quegli elementi mantenendo il loro ordine.

+=l'operatore chiama il __iadd__metodo nell'elenco. Richiede un iterabile e aggiunge tutti gli elementi dell'iterabile all'elenco in posizione. Non crea un nuovo oggetto elenco.

In classe fool'istruzione self.bar += [x]non è un'istruzione di assegnazione ma si traduce effettivamente in

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

che modifica l'elenco in posizione e agisce come il metodo list extend.

In classe foo2, al contrario, l'istruzione di assegnazione nel initmetodo

self.bar = self.bar + [x]  

può essere decostruito come:
L'istanza non ha alcun attributo bar(c'è un attributo di classe con lo stesso nome, però) quindi accede all'attributo di classe bare crea un nuovo elenco aggiungendolo xad esso. La dichiarazione si traduce in:

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

Quindi crea un attributo di istanza bare gli assegna l'elenco appena creato. Nota che bara destra dell'assegnazione è diverso da bara sinistra.

Per le istanze di classe foo, barè un attributo di classe e non un attributo di istanza. Pertanto, qualsiasi modifica all'attributo di classe barverrà riflessa per tutte le istanze.

Al contrario, ogni istanza della classe foo2ha il proprio attributo di istanza barche è diverso dall'attributo di classe con lo stesso nome bar.

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

Spero che questo chiarisca le cose.


5

Sebbene sia passato molto tempo e siano state dette molte cose corrette, non esiste una risposta che riunisca entrambi gli effetti.

Hai 2 effetti:

  1. un comportamento "speciale", forse inosservato, delle liste con +=(come affermato da Scott Griffiths )
  2. il fatto che siano coinvolti attributi di classe e attributi di istanza (come affermato da Can Berk Büder )

In classe foo, il __init__metodo modifica l'attributo di classe. È perché si self.bar += [x]traduce in self.bar = self.bar.__iadd__([x]). __iadd__()è per la modifica in loco, quindi modifica l'elenco e restituisce un riferimento ad esso.

Si noti che il dict di istanza viene modificato anche se normalmente non sarebbe necessario poiché il dict di classe contiene già lo stesso compito. Quindi questo dettaglio passa quasi inosservato, tranne se lo fai foo.bar = []dopo. Qui le istanze barrimangono le stesse grazie a questo fatto.

In classe foo2, tuttavia, la classe barviene utilizzata, ma non toccata. Invece, [x]viene aggiunto a, formando un nuovo oggetto, come self.bar.__add__([x])viene chiamato qui, che non modifica l'oggetto. Il risultato viene quindi inserito nel dict di istanza, dando all'istanza la nuova lista come un dict, mentre l'attributo della classe rimane modificato.

La distinzione tra ... = ... + ...e ... += ...influisce anche sulle assegnazioni successive:

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 

Puoi verificare l'identità degli oggetti con print id(foo), id(f), id(g)(non dimenticare i messaggi aggiuntivi ()se sei su Python3).

BTW: L' +=operatore è chiamato "assegnazione aumentata" e generalmente ha lo scopo di apportare modifiche in loco il più possibile.


5

Le altre risposte sembrerebbero averlo coperto, anche se sembra che valga la pena citare e fare riferimento all'Augmented Assignments PEP 203 :

Loro [gli operatori di assegnazione aumentata] implementano lo stesso operatore della loro normale forma binaria, tranne per il fatto che l'operazione viene eseguita "sul posto" quando l'oggetto sul lato sinistro lo supporta e che il lato sinistro viene valutato solo una volta.

...

L'idea alla base dell'assegnazione aumentata in Python è che non è solo un modo più semplice per scrivere la pratica comune di memorizzare il risultato di un'operazione binaria nel suo operando di sinistra, ma anche un modo per l'operando di sinistra in questione di sappi che dovrebbe operare "su se stesso", piuttosto che creare una copia modificata di se stesso.


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

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

Vediamo che quando proviamo a modificare un oggetto immutabile (intero in questo caso), Python ci fornisce semplicemente un oggetto diverso. D'altra parte, siamo in grado di apportare modifiche a un oggetto modificabile (una lista) e di mantenerlo sempre lo stesso oggetto.

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

Fare riferimento anche all'URL di seguito per comprendere la shallowcopy e la deepcopy

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


# L'ID è lo stesso per le liste
roshan ok
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.