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

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

Esto depende enteramente del objeto i .

+= llama al método __iadd__ (si existe, __add__ a __add__ si no existe), mientras que + llama al método __add__ o al método __radd__ en algunos casos 2 .

Desde la perspectiva de la API, se supone que __iadd__ se usa para modificar objetos mutables en su lugar (devolver 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á 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 boost i . En realidad, obtiene un nuevo número entero y lo asigna “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, 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] 

observe cómo en el primer ejemplo, dado que b y a referencia al mismo objeto, cuando uso += en b , en realidad cambia b (y a ve ese cambio – Después de todo, hace referencia a la misma lista). Sin embargo, en el segundo caso, cuando b = b + [1, 2, 3] , toma la lista a la 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 lo que b era la línea anterior.


1 En la expresión x + y , si x.__add__ no está implementado o si x.__add__(y) devuelve NotImplemented y x e y tienen diferentes tipos , entonces x + y trata de llamar a y.__radd__(x) . Así, en el caso de que tengas

foo_instance += bar_instance

Si Foo no 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 intentará antes de foo_instance.__add__ si el tipo de bar_instance es una subclase del tipo de foo_instance (por ejemplo, issubclass(Bar, Foo) ). Lo racional para esto es que Bar es, en cierto sentido, un objeto de “nivel superior” que Foo por lo que Bar debería tener la opción de anular el comportamiento de Foo .

Debajo de las cubiertas, i += 1 hace algo como esto:

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

Mientras que i = i + 1 hace algo como esto:

 i = i.__add__(1) 

Esto es una ligera simplificación, pero se tiene la idea: Python le da a los tipos una forma de manejar += especialmente, al crear un método __iadd__ así como un __add__ .

La intención es que los tipos mutables, como la list , se muten a sí mismos en __iadd__ (y luego se devuelvan, 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] 

Debido a que l2 es el mismo objeto que l1 , y mutaste a l1 , también mutaste a l2 .

Pero:

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

Aquí, no mutaste l1 ; en su lugar, creó una nueva lista, l1 + [3] , y rebote el nombre l1 para señalarlo, dejando que l2 apunte a la lista original.

(En la versión += , también estaba reencuadrando l1 , es solo que en ese caso estaba reencuadernándolo en la misma list que estaba vinculado, por lo que generalmente puede ignorar esa parte).

Aquí hay un ejemplo que 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] 

Si solo estás tratando con literales, entonces i += 1 tiene el mismo comportamiento que i = i + 1 .