Как работают операции 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
В частности:
- Это так же, как
a[i] = a[i] + x
? (которая не является операцией на месте) - Имеет ли это значение в этом случае, если
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.add
out
аргумент.
Это обсуждалось ранее, и 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