¿Cuándo es diferente "i += x" de "i = i + x" en Python?
Me dijeron que +=
puede tener efectos diferentes a los de la notación estándar i = i +
. ¿Hay algún caso en el que i += 1
sería diferente i = i + 1
?
Esto depende enteramente 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 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 incrementarse i
. En realidad, obtienes un nuevo número entero y lo asignas "encima de" i
, perdiendo una referencia al número entero anterior. En este caso, i += 1
es exactamente igual que i = i + 1
. Pero, con la mayoría de los objetos mutables, la historia es 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]
en comparación 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]
observe cómo en el primer ejemplo, dado que b
y a
hago referencia al mismo objeto, cuando uso +=
on b
, en realidad cambia b
(y a
ve ese cambio también; después de todo, hace referencia a la misma lista). Sin embargo, en el segundo caso, cuando lo hago b = b + [1, 2, 3]
, esto toma la lista a la que b
se 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 está implementada o si x.__add__(y)
los retornos NotImplemented
y x
tienen y
tipos diferentes , x + y
intenta llamar y.__radd__(x)
. Entonces, en el caso de que tengas
foo_instance += bar_instance
si Foo
no se implementa __add__
o __iadd__
entonces 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 probará antes foo_instance.__add__
si el tipo de bar_instance
es una subclase del tipo de foo_instance
(p.ej. issubclass(Bar, Foo)
). La razón de esto es que, Bar
en cierto sentido, un objeto de "nivel superior" Foo
debería Bar
tener la opción de anular Foo
el comportamiento de .
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 proporciona a los tipos una manera de manejarlos +=
de forma especial, mediante la creación de un __iadd__
método además de un archivo __add__
.
La intención es que los tipos mutables, como list
, se muten __iadd__
(y luego regresen self
, a menos que estés 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 mutaste l1
, 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 recuperó el nombre l1
para señalarlo, dejando l2
apuntando a la lista original.
(En la +=
versión, también estabas volviendo a vincular l1
, solo que en ese caso lo estabas volviendo a vincular al mismo modo al list
que ya estaba vinculado, por lo que generalmente puedes ignorar esa parte).