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] = 1
在 x
上执行 __setitem__
。
__getitem__
和__setitem__
可以defined/overloaded做任何事情。他们甚至不必彼此一致。
考虑以下数组操作:
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__(...)
.
根据
f(x[0])
中的x[0]
对x
执行__getitem__
。在这种特殊情况下(例如,与索引数组的 切片 相反),此操作返回的值不允许修改原始数组。x[0] = 1
在x
上执行__setitem__
。
__getitem__
和__setitem__
可以defined/overloaded做任何事情。他们甚至不必彼此一致。