¿Cuál es la diferencia entre i = i + 1 y i + = 1 en un bucle ‘for’?

Descubrí algo curioso hoy y me preguntaba si alguien podría arrojar algo de luz sobre cuál es la diferencia aquí.

import numpy as np A = np.arange(12).reshape(4,3) for a in A: a = a + 1 B = np.arange(12).reshape(4,3) for b in B: b += 1 

Después de ejecutar cada bucle for , A no ha cambiado, pero B ha añadido uno a cada elemento. De hecho, uso la versión B para escribir en una matriz NumPy inicializada dentro de un bucle for .

La diferencia es que uno modifica la estructura de datos en sí (operación in situ) b += 1 mientras que el otro simplemente reasigna la variable a = a + 1 .


Sólo para estar completo:

x += y no siempre realiza una operación in situ, hay (al menos) tres excepciones:

  • Si x no implementa un método __iadd__ , entonces la statement x += y es solo una abreviatura de x = x + y . Este sería el caso si x fuera algo así como un int .

  • Si __iadd__ devuelve No NotImplemented , Python retrocede a x = x + y .

  • El método __iadd__ podría teóricamente implementarse para no funcionar en su lugar. Sin embargo, sería realmente extraño hacer eso.

A medida que suceden, sus b s son numpy.ndarray s que implementan __iadd__ y se devuelven a sí mismos para que su segundo bucle modifique la matriz original en el lugar.

Puede leer más sobre esto en la documentación de Python de “Emulación de tipos numéricos” .

Estos __i*__ [ __i*__ ] se llaman para implementar las asignaciones aritméticas aumentadas ( += , -= *= , @= , /= , //= , %= , **= , <<= , >>= , &= , ^= , |= ). Estos métodos deben intentar realizar la operación en el lugar (modificarse el yo) y devolver el resultado (que podría ser, pero no tiene que serlo, el yo). Si no se define un método específico, la asignación aumentada vuelve a los métodos normales. Por ejemplo, si x es una instancia de una clase con un __iadd__() , x += y es equivalente a x = x.__iadd__(y) . De lo contrario, se considera x.__add__(y) y y.__radd__(x) , como en la evaluación de x + y . En ciertas situaciones, la asignación aumentada puede dar como resultado errores inesperados (consulte ¿Por qué a_tuple[i] += ["item"] genera una excepción cuando la adición funciona? ), Pero este comportamiento es en realidad parte del modelo de datos.

En el primer ejemplo, está reasignando la variable a , mientras que en el segundo está modificando los datos en el lugar, utilizando el operador += .

Vea la sección sobre 7.2.1. Declaraciones de asignación aumentada :

Una expresión de asignación aumentada como x += 1 puede reescribirse como x = x + 1 para lograr un efecto similar, pero no exactamente igual. En la versión aumentada, x solo se evalúa una vez. Además, cuando sea posible, la operación real se realiza en el lugar , lo que significa que en lugar de crear un nuevo objeto y asignarlo al objective, el objeto antiguo se modifica en su lugar.

+= operador llama a __iadd__ . Esta función realiza el cambio en el lugar, y solo después de su ejecución, el resultado se devuelve al objeto que está “aplicando” += activado.

__add__ otro lado, __add__ toma los parámetros y devuelve su sum (sin modificarlos).

Como ya se señaló, b += 1 actualiza b en el lugar, mientras que a = a + 1 calcula a + 1 y luego asigna el nombre a al resultado (ahora a ya no se refiere a una fila de A ).

Para entender correctamente el operador += , también necesitamos comprender el concepto de objetos mutables frente a objetos inmutables . Considere lo que sucede cuando dejamos de lado el .reshape :

 C = np.arange(12) for c in C: c += 1 print(C) # [ 0 1 2 3 4 5 6 7 8 9 10 11] 

Vemos que C no está actualizado, lo que significa que c += 1 y c = c + 1 son equivalentes. Esto se debe a que ahora C es una matriz 1D ( C.ndim == 1 ), y así cuando se itera sobre C , cada elemento entero se extrae y se asigna a c .

Ahora en Python, los enteros son inmutables, lo que significa que no se permiten las actualizaciones in situ, transformando efectivamente c += 1 en c = c + 1 , donde c ahora se refiere a un nuevo entero, no acoplado a C de ninguna manera. Cuando np.ndarray las matrices remodeladas, las filas completas ( np.ndarray s) se asignan a b (y a ) a la vez, que son objetos mutables , lo que significa que se le permite pegar nuevos enteros a voluntad, lo que sucede cuando hacer a += 1 .

Debe mencionarse que aunque + y += están relacionados como se describió anteriormente (y generalmente lo son), cualquier tipo puede implementarlos de la manera que desee definiendo los métodos __add__ y __iadd__ , respectivamente.

La forma abreviada ( a += 1 ) tiene la opción de modificar a in situ, en lugar de crear un nuevo objeto que represente la sum y volver a vincularlo con el mismo nombre ( a = a + 1 ). Por lo tanto, la forma corta ( a += 1 ) es muy eficiente, ya que no necesariamente tiene que hacer una copia de a diferencia de a = a + 1 .

Además, incluso si están dando el mismo resultado, observe que son diferentes porque son operadores separados: + y +=

En primer lugar, las variables a y b en los bucles se refieren a los objetos numpy.ndarray .

En el primer bucle, a = a + 1 se evalúa de la siguiente manera: se __add__(self, other) la función __add__(self, other) de numpy.ndarray . Esto crea un nuevo objeto y por lo tanto, A no se modifica. Después, la variable a se establece para referirse al resultado.

En el segundo bucle, no se crea ningún nuevo objeto. La statement b += 1 llama a la función __iadd__(self, other) de numpy.ndarray que modifica el objeto ndarray en lugar al que se refiere b. Por lo tanto, B se modifica.

Un problema clave aquí es que este bucle recorre las filas (primera dimensión) de B :

 In [258]: B Out[258]: array([[ 0, 1, 2], [ 3, 4, 5], [ 6, 7, 8], [ 9, 10, 11]]) In [259]: for b in B: ...: print(b,'=>',end='') ...: b += 1 ...: print(b) ...: [0 1 2] =>[1 2 3] [3 4 5] =>[4 5 6] [6 7 8] =>[7 8 9] [ 9 10 11] =>[10 11 12] 

Así, el += está actuando sobre un objeto mutable, una matriz.

Esto está implícito en las otras respuestas, pero se pierde fácilmente si se enfoca en la reasignación de a a = a+1 .

También podría hacer un cambio en lugar a b con [:] indexando, o incluso algo más sofisticado, b[1:]=0 :

 In [260]: for b in B: ...: print(b,'=>',end='') ...: b[:] = b * 2 [1 2 3] =>[2 4 6] [4 5 6] =>[ 8 10 12] [7 8 9] =>[14 16 18] [10 11 12] =>[20 22 24] 

Por supuesto, con una matriz 2d como B , normalmente no necesitamos iterar en las filas. Muchas operaciones que funcionan en una sola de B también funcionan en todo el asunto. B += 1 , B[1:] = 0 , etc.