Numpy 数组索引:查看或复制 - 取决于范围?

Numpy array indexing: view or copy - depends on scope?

考虑以下数组操作:

import numpy as np
def f(x):
     x += 1
x = np.zeros(1)
f(x)       # changes `x`
f(x[0])    # doesn't change `x`
x[0] += 1  # changes `x`

为什么 x[0] 的行为会因 += 1 发生在函数 f 内部还是外部而有所不同?

我可以将数组的一部分传递给函数,以便函数修改原始数组吗?


编辑:如果我们考虑 = 而不是 +=,我们可能会保留问题的核心,同时摆脱一些不相关的复杂性。

您甚至不需要函数调用就能看到这种差异。

x是一个数组:

In [138]: type(x)
Out[138]: numpy.ndarray

索引数组的一个元素 returns 一个 np.float64 对象。它实际上从数组中“取出”值;它不是对数组元素的引用。

In [140]: y=x[0]
In [141]: type(y)
Out[141]: numpy.float64

这个 y 很像一个 python 浮点数;你可以+=同样的方式:

In [142]: y += 1
In [143]: y
Out[143]: 1.0

但这不会改变x:

In [144]: x
Out[144]: array([0.])

但这确实改变了 x:

In [145]: x[0] += 1
In [146]: x
Out[146]: array([1.])

y=x[0] 调用 x.__getitem__x[0]=3 调用了 x.__setitem__+=使用__iadd__,但效果相似

另一个例子:

改变x

In [149]: x[0] = 3
In [150]: x
Out[150]: array([3.])

但尝试对 y 做同样的事情失败了:

In [151]: y[()] = 3
Traceback (most recent call last):
  File "<ipython-input-151-153d89268cbc>", line 1, in <module>
    y[()] = 3
TypeError: 'numpy.float64' object does not support item assignment

y[()] 是允许的。

basic 对带有切片的数组进行索引确实会产生可以修改的 view

In [154]: x = np.zeros(5)
In [155]: x
Out[155]: array([0., 0., 0., 0., 0.])
In [156]: y= x[0:2]
In [157]: type(y)
Out[157]: numpy.ndarray
In [158]: y += 1
In [159]: y
Out[159]: array([1., 1.])
In [160]: x
Out[160]: array([1., 1., 0., 0., 0.])

===

Python x[0]+=1 类操作的列表和字典示例:

In [405]: alist = [1,2,3]
In [406]: alist[1]+=12
In [407]: alist
Out[407]: [1, 14, 3]
In [408]: adict = {'a':32}
In [409]: adict['a'] += 12
In [410]: adict
Out[410]: {'a': 44}

__iadd__ 可以被认为是 __getitem__ 后跟具有相同索引的 __setitem__

问题不在于范围,因为唯一取决于范围的是可用名称。可以在具有名称的任何范围内访问所有对象。问题是可变性与不变性以及了解运算符的作用之一。

x 是一个可变的 numpy 数组。 f 直接在其上运行 x += 1+= 是调用就地加法的运算符。换句话说,它确实 x = x.__iadd__(1)*. Notice the reassignment to x, which happens in the function. That is a feature of the in-place operators that allows them to operate on immutable objects. In this case, ndarray.__iadd__ 是一个真正的就地运算符,它只是 returns x,并且一切都按预期工作。

现在我们用同样的方法来分析f(x[0])x[0] 对其调用 x.__getitem__(0)*. When you pass in a scalar int index, numpy extracts a one-element array and effectively calls .item()。结果是 python int(或 float,甚至可能是 tuple,具体取决于数组的 dtype 是什么)。无论哪种方式,对象都是不可变的。一旦它被 __getitem__ 提取,f 中的 += 运算符将 f 中的名称 x 替换为新对象,但在函数外部看不到变化,更不用说在数组中了。在这种情况下,f 没有引用 x,因此预计不会发生任何变化。

x[0] += 1的例子和调用f(x[0])不一样。相当于调用x.__setitem__(0, x.__getitem__(0).__iadd__(1))*。对 f 的调用只是 type(x).__getitem__(0).__iadd__(1) 的部分,returns 是一个新对象,但不会像 __setitem__ 那样重新分配。 关键是 python 中的 [] = (__setitem__) 是与 [] (__getitem__) 和 [=45= 完全不同的运算符](赋值)分别.

要使第二个示例 (f(x[0]) 正常工作,您必须传入一个可变对象。整数对象提取单个 python 对象,数组索引制作副本。但是,slice 索引 returns 是一个可变的视图,并绑定到原始数​​组内存。因此,你可以做

f(x[0:1])  # changes `x`

在这种情况下 f 执行以下操作:x.__getitem__(slice(0, 1, None)).__iadd__(1)。关键是 __getitem__ returns 原始数组的可变视图,而不是不可变的 int.

要了解为什么不仅对象是可变的而且它是原始数组的视图很重要,请尝试 f(x[[0]])。使用列表进行索引会生成一个数组,但会生成一个副本。 In x[[0]].__iadd__ 将修改您传入的列表,但不会将列表复制回原始列表,因此更改不会传播。


* 这是一个近似值。当由运算符调用时,dunder 方法实际上被称为 type(x).__operator__(x, ...),而不是 x.__operator__(...).

根据 and

  • f(x[0]) 中的 x[0]x 执行 __getitem__。在这种特殊情况下(例如,与索引数组的 切片 相反),此操作返回的值不允许修改原始数组。
  • x[0] = 1x 上执行 __setitem__

__getitem____setitem__可以defined/overloaded做任何事情。他们甚至不必彼此一致。