¿Cómo funcionan las operaciones en el lugar de numpy (por ejemplo, `+ =`)?

La pregunta básica es: ¿Qué sucede debajo del capó al hacer: a[i] += b ?

Dado lo siguiente:

 import numpy as np a = np.arange(4) i = a > 0 i = array([False, True, True, True], dtype=bool) 

Entiendo que:

  • a[i] = x es lo mismo que a.__setitem__(i, x) , que asigna directamente a los elementos indicados por i
  • a += x es lo mismo que a.__iadd__(x) , que hace la adición en su lugar

Pero que pasa cuando lo hago :

 a[i] += x 

Específicamente:

  1. ¿Es lo mismo que a[i] = a[i] + x ? (que no es una operación en el lugar)
  2. ¿Hay alguna diferencia en este caso si i es:
    • un índice int , o
    • un ndarray , o
    • un objeto de slice

Fondo

La razón por la que comencé a profundizar en esto es que encontré un comportamiento no intuitivo al trabajar con índices duplicados:

 a = np.zeros(4) x = np.arange(4) indices = np.zeros(4,dtype=np.int) # duplicate indices a[indices] += x a = array([ 3., 0., 0., 0.]) 

Más cosas interesantes sobre índices duplicados en esta pregunta .

Related of "¿Cómo funcionan las operaciones en el lugar de numpy (por ejemplo, `+ =`)?"

Lo primero que debe tener en cuenta es que a += x no se a.__iadd__(x) exactamente a a.__iadd__(x) , en su lugar, se asigna a a = a.__iadd__(x) . Tenga en cuenta que la documentación dice específicamente que los operadores en el lugar devuelven su resultado, y esto no tiene que ser self (aunque en la práctica, generalmente lo es). Esto significa que a[i] += x asigna trivialmente a:

 a.__setitem__(i, a.__getitem__(i).__iadd__(x)) 

Entonces, la adición técnicamente ocurre en el lugar, pero solo en un objeto temporal. Todavía hay potencialmente un objeto menos temporal creado que si se llamara __add__ , sin embargo.

En realidad eso no tiene nada que ver con el adormecimiento. No hay “set / getitem in-place” en python, estas cosas son equivalentes a a[indices] = a[indices] + x . Sabiendo eso, se vuelve bastante obvio lo que está pasando. (EDIT: como escribe lvc, en realidad el lado derecho está en su lugar, de modo que es a[indices] = (a[indices] += x) si esa fue una syntax legal, eso tiene un efecto bastante similar)

Por supuesto, a += x realidad está en el lugar, asignando a al argumento np.add out .

Se ha discutido antes y Numpy no puede hacer nada al respecto como tal. Aunque hay una idea de tener un np.add.at(array, index_expression, x) para al menos permitir tales operaciones.

Como explica Ivc, no hay un método para agregar elementos en el lugar, por lo que bajo el capó usa __getitem__ , luego __iadd__ , luego __setitem__ . Aquí hay una manera de observar empíricamente ese comportamiento:

 import numpy class A(numpy.ndarray): def __getitem__(self, *args, **kwargs): print "getitem" return numpy.ndarray.__getitem__(self, *args, **kwargs) def __setitem__(self, *args, **kwargs): print "setitem" return numpy.ndarray.__setitem__(self, *args, **kwargs) def __iadd__(self, *args, **kwargs): print "iadd" return numpy.ndarray.__iadd__(self, *args, **kwargs) a = A([1,2,3]) print "about to increment a[0]" a[0] += 1 

Se imprime

 about to increment a[0] getitem iadd setitem 

Creo que la principal diferencia aquí es que los operadores en el lugar pueden devolver la misma referencia, pero el efecto es diferente en NumPy que en Python.

Comience con Python

 >>> a = 1 >>> b = a >>> a is b True 

Estas son las mismas referencias.

 >>> a += 4 >>> a 5 >>> b 1 

En su lugar la adición crea una nueva referencia.

Ahora para NumPy

 >>> import numpy as np >>> a = np.array([1, 2, 3], float) >>> b = a >>> a is b True 

Nuevamente, estos son la misma referencia, pero en lugar los operadores tienen un efecto diferente.

 >>> a += 4 >>> a array([ 5., 6., 7.]) >>> b array([ 5., 6., 7.]) 

En lugar de la adición de un ndarray actualiza la referencia. No es lo mismo que llamar a numpy.add que crea una copia en una nueva referencia.

 >>> a = a + 4 >>> a array([ 9., 10., 11.]) >>> b array([ 5., 6., 7.]) 

Operaciones in situ sobre referencias prestadas

El peligro aquí es si la referencia se pasa a un scope diferente.

 >>> def f(x): ... x += 4 ... return x 

La referencia del argumento a x se pasa al scope de f que no hace una copia y, de hecho, cambia el valor en esa referencia y lo devuelve.

 >>> f(a) array([ 13., 14., 15.]) >>> f(a) array([ 17., 18., 19.]) >>> f(a) array([ 21., 22., 23.]) >>> f(a) array([ 25., 26., 27.]) 

Esto puede ser confuso, por lo que solo use operadores locales en las referencias que pertenecen al scope actual y tenga cuidado con las referencias prestadas.