Как работают операции numpy in-place (например, `+ =`)?
Основной вопрос: что происходит под капотом при выполнении: 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
В частности:
- Это то же самое, что
a[i] = a[i] + x
? (что не является операцией на месте)
- В этом случае имеет значение, если
i
:
- a
int
index или
- a
ndarray
или
- a
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.])
Более интересный материал о дублирующих индексах в этом вопросе.
Ответы
Ответ 1
Первое, что вам нужно понять, это то, что a += x
точно не соответствует a.__iadd__(x)
, вместо этого он отображается на a = a.__iadd__(x)
. Обратите внимание, что документация специально говорит, что операторы на месте возвращают их результат, и это не должно быть self
(хотя на практике это обычно есть). Это означает, что a[i] += x
тривиально отображает:
a.__setitem__(i, a.__getitem__(i).__iadd__(x))
Итак, добавление технически происходит на месте, но только на временном объекте. По-прежнему существует потенциально один менее временный объект, чем если бы он назывался __add__
.
Ответ 2
Собственно, это не имеет ничего общего с numpy. В python нет "set/getitem in-place", это эквивалентно a[indices] = a[indices] + x
. Зная это, становится довольно очевидным, что происходит. (EDIT: Как пишет lvc, на самом деле правая сторона находится на своем месте, так что это a[indices] = (a[indices] += x)
, если это легальный синтаксис, который имеет почти такой же эффект, хотя)
Конечно, a += x
действительно является на месте, сопоставляя a с аргументом np.add
out
.
Это обсуждалось ранее, и numpy не может ничего с этим поделать. Хотя есть идея иметь np.add.at(array, index_expression, x)
, чтобы хотя бы разрешить такие операции.
Ответ 3
Как объясняет 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
Ответ 4
Я не знаю, что происходит внутри, но операции на месте над элементами в массивах NumPy и в списках Python возвращают одну и ту же ссылку, что может привести к путанице в IMO при передаче в функцию.
Начать с Python
>>> a = [1, 2, 3]
>>> b = a
>>> a is b
True
>>> id(a[2])
12345
>>> id(b[2])
12345
... где 12345
- это уникальный id
для местоположения значения в a[2]
в памяти, которое совпадает с b[2]
.
Таким образом, a
и b
относятся к одному и тому же списку в памяти. Теперь попробуйте добавить на месте элемент в списке.
>>> a[2] += 4
>>> a
[1, 2, 7]
>>> b
[1, 2, 7]
>>> a is b
True
>>> id(a[2])
67890
>>> id(b[2])
67890
Таким образом, добавление элемента в список только на месте изменило значение элемента в индексе 2
, но a
и b
все еще ссылаются на тот же список, хотя 3-й элемент в списке был переназначен на новое значение, 7
. Переназначение объясняет, почему, если a = 4
и b = a
были целыми числами (или числами с плавающей запятой) вместо списков, тогда a += 1
приведет к переназначению a
, а затем b
и a
будут разными ссылками. Однако, если вызывается добавление в список, например: a += [5]
для a
и b
ссылающихся на один и тот же список, оно не переназначает a
; они оба будут добавлены.
Теперь для NumPy
>>> import numpy as np
>>> a = np.array([1, 2, 3], float)
>>> b = a
>>> a is b
True
Опять же, это та же ссылка, и операторы на месте, похоже, имеют тот же эффект, что и для списка в Python:
>>> 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.])
То же самое можно сказать и о списке Python:
>>> def f(x, y):
... x += [y]
>>> a = [1, 2, 3]
>>> b = a
>>> f(a, 5)
>>> a
[1, 2, 3, 5]
>>> b
[1, 2, 3, 5]
В IMO это может сбивать с толку, а иногда и затруднять отладку, поэтому я стараюсь использовать операторы на месте только для ссылок, принадлежащих текущей области, и стараюсь быть осторожным с заимствованными ссылками.