numpy的就地操作(例如`+ =`)如何工作?

shx*_*hx2 14 python numpy

基本问题是:在做什么时会发生什么a[i] += b

鉴于以下内容:

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

我明白那个:

  • a[i] = x是相同的a.__setitem__(i, x),它直接分配给指示的项目i
  • a += x是一样的a.__iadd__(x),它不到位此外

但是当我这样做时会发生什么:

a[i] += x
Run Code Online (Sandbox Code Playgroud)

特别:

  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.])
Run Code Online (Sandbox Code Playgroud)

关于这个问题中重复索引的更有趣的东西.

lvc*_*lvc 12

您需要意识到的第一件事是a += x不完全映射到a.__iadd__(x),而是映射到a = a.__iadd__(x).请注意,文档特别指出就地操作符返回其结果,而这不是必须的self(尽管在实践中通常是这样).这意味着a[i] += x简单地映射到:

a.__setitem__(i, a.__getitem__(i).__iadd__(x))
Run Code Online (Sandbox Code Playgroud)

因此,添加技术上就地发生,但仅限于临时对象.但是__add__,创建的临时对象可能比调用时少一个.

  • 很好,谢谢你的提示!运行 a.__iadd__(a) 是否有任何危险? (2认同)

Mar*_*ski 6

我不知道幕后发生了什么,但对 NumPy 数组和 Python 列表中的项目进行就地操作将返回相同的引用,在我看来,当传递给函数时,这可能会导致令人困惑的结果。

从Python开始

>>> a = [1, 2, 3]
>>> b = a
>>> a is b
True
>>> id(a[2])
12345
>>> id(b[2])
12345
Run Code Online (Sandbox Code Playgroud)

... where12345是值 at 在内存中的唯一id位置a[2],与 相同b[2]

所以ab引用内存中的同一个列表。现在尝试对列表中的项目进行就地添加。

>>> a[2] += 4
>>> a
[1, 2, 7]
>>> b
[1, 2, 7]
>>> a is b
True
>>> id(a[2])
67890
>>> id(b[2])
67890
Run Code Online (Sandbox Code Playgroud)

因此,列表中项目的就地添加仅更改了索引处项目的值2,但a仍然b引用相同的列表,尽管列表中的第三个项目被重新分配给新值7。重新分配解释了为什么如果a = 4b = a是整数(或浮点数)而不是列表,那么a += 1会导致a重新分配,然后ba会是不同的引用。但是,如果调用列表添加,例如a += [5]forab引用同一列表,则不会重新分配a;它们都将被附加。

现在介绍 NumPy

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

同样,这些是相同的引用,并且就地运算符似乎与 Python 中的列表具有相同的效果:

>>> a += 4
>>> a
array([ 5.,  6.,  7.])
>>> b
array([ 5.,  6.,  7.])
Run Code Online (Sandbox Code Playgroud)

添加更新ndarray参考。numpy.add这与在新引用中创建副本的调用不同。

>>> a = a + 4
>>> a
array([  9.,  10.,  11.])
>>> b
array([ 5.,  6.,  7.])
Run Code Online (Sandbox Code Playgroud)

对借用的引用进行就地操作

我认为这里的危险在于引用被传递到不同的范围。

>>> def f(x):
...     x += 4
...     return x
Run Code Online (Sandbox Code Playgroud)

参数引用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.])
Run Code Online (Sandbox Code Playgroud)

对于 Python 列表也是如此:

>>> a = [1, 2, 3]
>>> b = a
>>> a is b
True
>>> id(a[2])
12345
>>> id(b[2])
12345
Run Code Online (Sandbox Code Playgroud)

在我看来,这可能会令人困惑,有时难以调试,因此我尝试仅对属于当前范围的引用使用就地运算符,并且我尝试小心借用的引用。


seb*_*erg 5

其实这与numpy无关。python中没有“set/getitem in-place”,这些东西等价于a[indices] = a[indices] + x. 知道了这一点,发生的事情就变得很明显了。(编辑:正如 lvc 所写,实际上右手边已经到位,所以a[indices] = (a[indices] += x)如果这是合法的语法,但效果大致相同)

当然a += x实际上是就地的,通过将 a 映射到np.add out参数。

之前已经讨论过,numpy 对此无能为力。虽然有一个想法np.add.at(array, index_expression, x)至少允许这样的操作。

  • ufunc 的减少已添加到 numpy 1.8.0 (2认同)
  • @seberg:您确定 a[:] = a[:]+x 与 a+=x 相同吗?它们肯定会产生相同的结果,但除非我犯了错误,否则后一个版本对于大型数组来说要快得多。 (2认同)