¿Cuándo es "i + = x" diferente de "i = i + x" en Python?

212

Me dijeron que +=puede tener diferentes efectos que la notación estándar de i = i +. ¿Hay algún caso en el que i += 1sería diferente i = i + 1?

MarJamRob
fuente
77
+=actúa como extend()en el caso de las listas.
Ashwini Chaudhary
12
@AshwiniChaudhary Esa es una distinción bastante sutil, considerando que i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6]es así True. Muchos desarrolladores pueden no notar los id(i)cambios para una operación, pero no para la otra.
kojiro
1
@kojiro - Si bien es una distinción sutil, creo que es importante.
mgilson
@mgilson es importante, así que sentí que necesitaba una explicación. :)
kojiro
1
Pregunta relacionada con respecto a las diferencias entre los dos en Java: stackoverflow.com/a/7456548/245966
jakub.g

Respuestas:

317

Esto depende completamente del objeto i.

+=llama al __iadd__método (si existe, recurriendo __add__si no existe) mientras que +llama al __add__método 1 o al __radd__método en algunos casos 2 .

Desde una perspectiva API, __iadd__se supone que se usa para modificar objetos mutables en su lugar (devolviendo el objeto que fue mutado), mientras que __add__debería devolver una nueva instancia de algo. Para los objetos inmutables , ambos métodos devuelven una nueva instancia, pero __iadd__colocarán la nueva instancia en el espacio de nombres actual con el mismo nombre que tenía la instancia anterior. Esta es la razón por

i = 1
i += 1

Parece aumentar i. En realidad, obtienes un nuevo entero y lo asignas "encima" i, perdiendo una referencia al entero anterior. En este caso, i += 1es exactamente lo mismo que i = i + 1. Pero, con la mayoría de los objetos mutables, es una historia diferente:

Como ejemplo 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]

comparado con:

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

Nótese como en el primer ejemplo, puesto by ahacer referencia al mismo objeto, cuando se utiliza +=en b, lo que realmente cambia b(y ave que el cambio también - Después de todo, se hace referencia a la misma lista). Sin embargo, en el segundo caso, cuando lo hago b = b + [1, 2, 3], toma la lista que bhace referencia y la concatena con una nueva lista [1, 2, 3]. Luego almacena la lista concatenada en el espacio de nombres actual como b: sin tener en cuenta cuál bera la línea anterior.


1 En la expresión x + y, si x.__add__no se ejecuta o si x.__add__(y)vuelve NotImplemented y xy ytienen diferentes tipos , a continuación, x + yintenta llamar y.__radd__(x). Entonces, en el caso donde tienes

foo_instance += bar_instance

si Foono se implementa __add__o __iadd__el resultado aquí es el mismo que

foo_instance = bar_instance.__radd__(bar_instance, foo_instance)

2 En la expresión foo_instance + bar_instance, bar_instance.__radd__se intentará antes foo_instance.__add__ si el tipo de bar_instancees una subclase del tipo de foo_instance(por ejemplo issubclass(Bar, Foo)). El racional para esto es porque Bares en cierto sentido un objeto "nivel más alto" que Fooasí Bardebe recibir la opción de anular Fooel comportamiento 's.

mgilson
fuente
18
Bueno, +=llama __iadd__ si existe , y de lo contrario vuelve a agregar y volver a vincular. Por eso i = 1; i += 1funciona a pesar de que no hay int.__iadd__. Pero aparte de esa pequeña mentira, grandes explicaciones.
abarnert
44
@abarnert: siempre supuse que int.__iadd__acababa de llamar __add__. Me alegro de haber aprendido algo nuevo hoy :).
mgilson
@abarnert - supongo que tal vez para ser completado , x + ylas llamadas y.__radd__(x)si x.__add__no existe (o devoluciones NotImplementedy xy yson de diferentes tipos)
mgilson
Si realmente quieres ser completista, deberías mencionar que el bit "si existe" pasa por los mecanismos getattr habituales, a excepción de algunas peculiaridades con las clases clásicas, y para los tipos implementados en la API de C, en su lugar, busca nb_inplace_addo sq_inplace_concat, y esas funciones de API C tienen requisitos más estrictos que los métodos dunder de Python, y ... Pero no creo que sea relevante para la respuesta. La principal distinción es que +=intenta hacer un agregado en el lugar antes de volver a actuar como +, lo que creo que ya has explicado.
abarnert
Sí, supongo que tienes razón ... Aunque podría retroceder en la postura de que la API de C no es parte de Python . Es parte de CPython :-P
mgilson
67

Debajo de las sábanas, i += 1hace algo como esto:

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

Mientras i = i + 1hace algo como esto:

i = i.__add__(1)

Esta es una ligera simplificación excesiva, pero se entiende la idea: Python le da a los tipos una forma de manejarlos +=especialmente, creando un __iadd__método y un __add__.

La intención es que los tipos mutables, como list, se muten a sí mismos __iadd__(y luego regresen self, a menos que esté haciendo algo muy complicado), mientras que los tipos inmutables, como int, simplemente no lo implementarán.

Por ejemplo:

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

Porque l2es el mismo objeto que l1, y tú mutaste l1, tú también mutaste l2.

Pero:

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

Aquí no mutaste l1; en su lugar, creó una nueva lista l1 + [3]y rebotó el nombre l1para señalarla, dejando de l2apuntar a la lista original.

(En la +=versión, también estaba volviendo a vincular l1, es solo que en ese caso lo estaba volviendo a listvincular a la misma que ya estaba vinculada, por lo que generalmente puede ignorar esa parte).

abarnert
fuente
en __iadd__realidad llama __add__en caso de un AttributeError?
mgilson
Bueno, i.__iadd__no llama __add__; Es lo i += 1que llama __add__.
abarnert
errr ... Sí, eso es lo que quise decir. Interesante. No me di cuenta de que se hizo automáticamente.
mgilson
3
El primer intento es en realidad i = i.__iadd__(1): iadd puede modificar el objeto en su lugar, pero no tiene que hacerlo, por lo que se espera que devuelva el resultado en cualquier caso.
lvc
Tenga en cuenta que esto significa que operator.iaddlas llamadas __add__sobre AttributeError, pero no pueden volver a vincular el resultado ... así que i=1; operator.iadd(i, 1)vuelve 2 y hojas ifijan a 1. Lo cual es un poco confuso.
abarnert
6

Aquí hay un ejemplo que se compara directamente 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]
Deqing
fuente