Как работают операции Numpy на месте (например, `+=`)?

Основной вопрос: что происходит под капотом при выполнении: a[i] += b?

Учитывая следующее:

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

Я это понимаю:

  • a[i] = x такой же как a.__setitem__(i, x), который присваивает непосредственно элементам, указанным i
  • a += x такой же как a.__iadd__(x), который делает сложение на месте

Но что происходит, когда я делаю:

a[i] += x

В частности:

  1. Это так же, как a[i] = a[i] + x? (которая не является операцией на месте)
  2. Имеет ли это значение в этом случае, если i является:
    • int индекс или
    • ndarray, или же
    • slice объект

Фон

Причина, по которой я начал вникать в это, заключается в том, что я столкнулся с неинтуитивным поведением при работе с дублирующимися индексами:

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.])

Более интересный материал о дублирующих индексах в этом вопросе.

4 ответа

Решение

Первое, что вам нужно понять, это то, что a += x не отображается точно на a.__iadd__(x)вместо этого он отображается на a = a.__iadd__(x), Обратите внимание, что в документации конкретно сказано, что операторы на месте возвращают свой результат, и это не обязательно self (хотя на практике это обычно так). Это означает a[i] += x тривиально сопоставляется с:

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

Итак, сложение технически происходит на месте, но только на временном объекте. Возможно, еще один созданный временный объект меньше, чем если бы он вызывался __add__, хоть.

На самом деле это не имеет ничего общего с NumPy. В Python нет "set/getitem in-place", эти вещи эквивалентны a[indices] = a[indices] + x, Зная это, становится совершенно очевидно, что происходит. (РЕДАКТИРОВАТЬ: Как пишет lvc, на самом деле правая сторона на месте, так что это a[indices] = (a[indices] += x) если бы это был законный синтаксис, это бы имело в значительной степени тот же эффект)

Конечно a += x на самом деле на месте, путем сопоставления с np.addout аргумент.

Это обсуждалось ранее, и numpy ничего не может с этим поделать. Хотя есть идея иметь np.add.at(array, index_expression, x) по крайней мере, разрешить такие операции.

Я думаю, что основное отличие здесь заключается в том, что операторы на месте могут возвращать одну и ту же ссылку, но эффект в NumPy отличается от Python.

Начать с Python

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

Это одна и та же ссылка.

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

На месте сложения создается новая ссылка.

Теперь для NumPy

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

Опять же, это одна и та же ссылка, но операторы на месте имеют другой эффект.

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

Вместо добавления ndarray обновляется ссылка. Это не то же самое, что звонить numpy.add который создает копию в новой ссылке.

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

Операции на месте по заимствованным ссылкам

Опасность здесь в том, что ссылка передается в другую область.

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

Ссылка на аргумент x передается в сферу f который не делает копию и фактически изменяет значение в этой ссылке и передает его обратно.

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

Это может сбивать с толку, поэтому используйте операторы на месте только для ссылок, которые принадлежат текущей области, и будьте осторожны с заимствованными ссылками.

Как объясняет Ivc, метода добавления элементов на месте не существует, поэтому под капотом он использует __getitem__, затем __iadd__, затем __setitem__, Вот способ эмпирически наблюдать это поведение:

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

Это печатает

about to increment a[0]
getitem
iadd
setitem
Другие вопросы по тегам