19

基本问题是:在执行以下操作时,幕后会发生什么: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

4 回答 4

17

您需要意识到的第一件事是它a += x并不完全映射到a.__iadd__(x),而是映射到a = a.__iadd__(x)。请注意,文档明确指出就地运算符返回其结果,这不一定是self(尽管在实践中,它通常是)。这意味着a[i] += x微不足道地映射到:

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

因此,从技术上讲,添加发生在原地,但仅在临时对象上。但是,与调用 相比,创建的临时对象仍然可能少一个__add__

于 2013-04-16T10:59:41.287 回答
5

实际上这与numpy无关。python 中没有“就地设置/获取项目”,这些东西相当于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)至少允许这样的操作。

于 2013-04-16T10:53:16.983 回答
3

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

从 Python 开始

>>> a = [1, 2, 3]
>>> b = a
>>> a is b
True
>>> id(a[2])
12345
>>> id(b[2])
12345

... where是值在内存中12345的唯一位置,与.ida[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

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

现在为 NumPy

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

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

>>> 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 这可能会令人困惑,有时难以调试,因此我尝试仅对属于当前范围的引用使用就地运算符,并尝试小心借用引用。

于 2016-05-04T23:55:31.733 回答
2

正如 Ivc 解释的那样,没有就地项目添加方法,所以在它的底层使用__getitem__, then __iadd__, then __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
于 2013-04-16T11:10:51.843 回答