Quando "i + = x" è diverso da "i = i + x" in Python?


212

Mi è stato detto che +=possono avere effetti diversi rispetto alla notazione standard di i = i +. C'è un caso in cui i += 1sarebbe diverso i = i + 1?


7
+=si comporta come extend()in caso di liste.
Ashwini Chaudhary,

12
@AshwiniChaudhary È una distinzione abbastanza sottile, considerando che lo i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6]è True. Molti sviluppatori potrebbero non notare che id(i)cambia per un'operazione, ma non per l'altra.
Kojiro,

1
@kojiro - Anche se è una sottile distinzione, penso che sia importante.
mgilson,

@mgilson è importante e quindi ho sentito che aveva bisogno di una spiegazione. :)
Kojiro

1
Questione connessa per quanto riguarda le differenze tra i due in Java: stackoverflow.com/a/7456548/245966
jakub.g

Risposte:


317

Questo dipende interamente dall'oggetto i.

+=chiama il __iadd__metodo (se esiste - riconnettendosi __add__se non esiste) mentre +chiama il __add__metodo 1 o il __radd__metodo in alcuni casi 2 .

Dal punto di vista dell'API, __iadd__dovrebbe essere usato per modificare gli oggetti mutabili sul posto (restituendo l'oggetto che è stato mutato) mentre __add__dovrebbe restituire una nuova istanza di qualcosa. Per oggetti immutabili , entrambi i metodi restituiscono una nuova istanza, ma __iadd__inseriranno la nuova istanza nello spazio dei nomi corrente con lo stesso nome della vecchia istanza. Ecco perché

i = 1
i += 1

sembra aumentare i. In realtà, ottieni un nuovo numero intero e lo assegni "sopra" i- perdendo un riferimento al vecchio numero intero. In questo caso, i += 1è esattamente lo stesso di i = i + 1. Ma, con la maggior parte degli oggetti mutabili, è una storia diversa:

A titolo di esempio concreto:

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]

rispetto a:

a = [1, 2, 3]
b = a
b = b + [1, 2, 3]
print a #[1, 2, 3]
print b #[1, 2, 3, 1, 2, 3]

nota come nel primo esempio, dato che be ariferimento allo stesso oggetto, quando lo uso +=su b, in realtà cambia b(e avede anche quel cambiamento - Dopotutto, fa riferimento allo stesso elenco). Nel secondo caso, tuttavia, quando lo faccio b = b + [1, 2, 3], prende l'elenco che bfa riferimento e lo concatena con un nuovo elenco [1, 2, 3]. Quindi memorizza l'elenco concatenato nello spazio dei nomi corrente come b- Senza riguardo a quella che bera la linea prima.


1 Nell'espressione x + y, se x.__add__non è attuato o se x.__add__(y)i rendimenti NotImplemented e xed yhanno tipi diversi , quindi x + ytenta di chiamare y.__radd__(x). Quindi, nel caso in cui tu abbia

foo_instance += bar_instance

se Foonon implementa __add__o __iadd__quindi il risultato qui è lo stesso di

foo_instance = bar_instance.__radd__(bar_instance, foo_instance)

2 Nell'espressione foo_instance + bar_instance, bar_instance.__radd__verrà provato prima foo_instance.__add__ se il tipo di bar_instanceè una sottoclasse del tipo di foo_instance(ad es issubclass(Bar, Foo).). Il razionale di questo è perché Barè in un certo senso un oggetto "di livello superiore" rispetto Foocosì Bardovrebbe ottenere la possibilità di sovrascrivere Foo's comportamento.


18
Bene, +=chiama __iadd__ se esiste , e ricade nell'aggiungere e nel rifare altrimenti. Ecco perché i = 1; i += 1funziona anche se non c'è int.__iadd__. Ma a parte quel piccolo problema, grandi spiegazioni.
Abarnert,

4
@abarnert - Ho sempre pensato che avesse int.__iadd__appena chiamato __add__. Sono felice di aver imparato qualcosa di nuovo oggi :).
mgilson

@abarnert - Suppongo che forse sia completo , x + ychiama y.__radd__(x)se x.__add__non esiste (o restituisce NotImplementede xe ysono di diversi tipi)
mgilson

Se vuoi davvero essere completo, dovresti menzionare che il bit "se esiste" passa attraverso i soliti meccanismi getattr, ad eccezione di alcune stranezze con classi classiche, e per i tipi implementati nell'API C cerca invece nb_inplace_addoppure sq_inplace_concat, e quelle funzioni dell'API C hanno requisiti più severi rispetto ai metodi Dunder di Python, e ... Ma non credo sia rilevante per la risposta. La principale distinzione è che +=cerca di fare un'aggiunta sul posto prima di ricominciare ad agire come +, cosa che penso tu abbia già spiegato.
Abarnert,

Sì, suppongo tu abbia ragione ... Anche se potrei semplicemente ricadere sulla posizione secondo cui l'API C non fa parte di Python . Fa parte di Cpython :-P
mgilson

67

Sotto le coperte, i += 1fa qualcosa del genere:

try:
    i = i.__iadd__(1)
except AttributeError:
    i = i.__add__(1)

Mentre i = i + 1fa qualcosa del genere:

i = i.__add__(1)

Questa è una leggera semplificazione eccessiva, ma hai l'idea: Python offre ai tipi un modo di gestire in modo +=speciale, creando un __iadd__metodo oltre che un __add__.

L'intenzione è che i tipi mutabili, come list, si trasformino __iadd__(e poi ritornino self, a meno che tu non stia facendo qualcosa di molto complicato), mentre i tipi immutabili, come int, non lo implementeranno.

Per esempio:

>>> l1 = []
>>> l2 = l1
>>> l1 += [3]
>>> l2
[3]

Perché l2è lo stesso oggetto di l1, e tu sei mutato l1, anche tu sei mutato l2.

Ma:

>>> l1 = []
>>> l2 = l1
>>> l1 = l1 + [3]
>>> l2
[]

Qui, non hai mutato l1; invece, hai creato un nuovo elenco l1 + [3]e rimbalzato il nome l1per puntarlo, lasciando il l2puntamento sull'elenco originale.

(Nella +=versione, stavi anche eseguendo il rebinding l1, è solo che in quel caso lo stavi legando allo stesso a listcui era già legato, quindi di solito puoi ignorare quella parte.)


non __iadd__realmente chiamare __add__in caso di un AttributeError?
mgilson

Bene, i.__iadd__non chiama __add__; è i += 1quello che chiama __add__.
Abarnert,

errr ... Sì, questo è ciò che intendevo. Interessante. Non mi rendevo conto che era stato fatto automaticamente.
mgilson

3
Il primo tentativo è in realtà i = i.__iadd__(1): iadd può modificare l'oggetto sul posto, ma non è necessario, quindi ci si aspetta che restituisca il risultato in entrambi i casi.
PVC

Si noti che questo significa che operator.iaddle chiamate __add__su AttributeError, ma non può associare nuovamente il risultato ... quindi i=1; operator.iadd(i, 1)restituisce 2 e foglie iimpostati 1. Il che è un po 'confuso.
Abarnert,

6

Ecco un esempio che si confronta direttamente i += xcon 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]
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.