Me dijeron que +=
puede tener diferentes efectos que la notación estándar de i = i +
. ¿Hay algún caso en el que i += 1
sería diferente i = i + 1
?
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 += 1
es 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 b
y a
hacer referencia al mismo objeto, cuando se utiliza +=
en b
, lo que realmente cambia b
(y a
ve 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 b
hace 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 b
era 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 x
y y
tienen diferentes tipos , a continuación, x + y
intenta llamar y.__radd__(x)
. Entonces, en el caso donde tienes
foo_instance += bar_instance
si Foo
no 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_instance
es una subclase del tipo de foo_instance
(por ejemplo issubclass(Bar, Foo)
). El racional para esto es porque Bar
es en cierto sentido un objeto "nivel más alto" que Foo
así Bar
debe recibir la opción de anular Foo
el comportamiento 's.
+=
llama __iadd__
si existe , y de lo contrario vuelve a agregar y volver a vincular. Por eso i = 1; i += 1
funciona a pesar de que no hay int.__iadd__
. Pero aparte de esa pequeña mentira, grandes explicaciones.
int.__iadd__
acababa de llamar __add__
. Me alegro de haber aprendido algo nuevo hoy :).
x + y
las llamadas y.__radd__(x)
si x.__add__
no existe (o devoluciones NotImplemented
y x
y y
son de diferentes tipos)
nb_inplace_add
o 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.
Debajo de las sábanas, i += 1
hace algo como esto:
try:
i = i.__iadd__(1)
except AttributeError:
i = i.__add__(1)
Mientras i = i + 1
hace 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 l2
es 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 l1
para señalarla, dejando de l2
apuntar 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 list
vincular a la misma que ya estaba vinculada, por lo que generalmente puede ignorar esa parte).
__iadd__
realidad llama __add__
en caso de un AttributeError
?
i.__iadd__
no llama __add__
; Es lo i += 1
que llama __add__
.
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.
operator.iadd
las llamadas __add__
sobre AttributeError
, pero no pueden volver a vincular el resultado ... así que i=1; operator.iadd(i, 1)
vuelve 2 y hojas i
fijan a 1
. Lo cual es un poco confuso.
Aquí hay un ejemplo que se compara directamente i += x
con 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]
+=
actúa comoextend()
en el caso de las listas.i=[1,2,3];i=i+[4,5,6];i==[1,2,3,4,5,6]
es asíTrue
. Muchos desarrolladores pueden no notar losid(i)
cambios para una operación, pero no para la otra.