Подтвердить что ты не робот

Как работают операции 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.])

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

4b9b3361

Ответ 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 это может сбивать с толку, а иногда и затруднять отладку, поэтому я стараюсь использовать операторы на месте только для ссылок, принадлежащих текущей области, и стараюсь быть осторожным с заимствованными ссылками.