NumPy 中的视图与副本

日期2018-02-11(最后修改),2008-01-31(创建)

人们经常给 !NumPy 列表发邮件询问在哪些情况下会创建数组的视图,哪些情况下不会。本页试图澄清关于这个相当微妙主题的一些棘手问题。

什么是 NumPy 数组的视图?

顾名思义,它只是另一种 **查看** 数组数据的 **方式**。从技术上讲,这意味着两个对象的 **数据** 是 **共享** 的。您可以通过选择原始数组的 **切片** 来创建视图,也可以通过更改 **dtype**(或两者结合)来创建视图。这些不同类型的视图将在下面介绍。

切片视图

这可能是 !NumPy 中创建视图最常见的来源。创建切片视图的经验法则是,被查看的元素可以用原始数组中的偏移量、步长和计数来寻址。例如

在 [ ]
>>> a = numpy.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> v1 = a[1:2]
>>> v1
array([1])
>>> a[1] = 2
>>> v1
array([2])
>>> v2 = a[1::3]
>>> v2
array([2, 4, 7])
>>> a[7] = 10
>>> v2
array([ 2,  4, 10])

在上面的代码片段中,v1 和 v2 是 a 的视图。如果您更新 a 的元素,那么 v1 和 v2 将反映更改。

Dtype 视图

另一种创建数组视图的方法是将另一个 **dtype** 分配给同一个数据区域。例如

在 [ ]
>>> b = numpy.arange(10, dtype='int16')
>>> b
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int16)
>>> v3 = b.view('int32')
>>> v3 += 1
>>> b
array([1, 1, 3, 3, 5, 5, 7, 7, 9, 9], dtype=int16)
>>> v4 = b.view('int8')
>>> v4
array([1, 0, 1, 0, 3, 0, 3, 0, 5, 0, 5, 0, 7, 0, 7, 0, 9, 0, 9, 0], dtype=int8)

在这种情况下,和是 的视图。如您所见,**dtype 视图** 不如 **切片视图** 那么有用,但在某些情况下可能很方便(例如,快速查看通用数组的字节)。

常见问题解答

我认为我理解了视图的概念,但为什么花式索引没有返回视图?

人们可能会倾向于认为使用花式索引会导致切片视图。但事实并非如此。

在 [ ]
>>> a = numpy.arange(10)
>>> c1 = a[[1,3]]
>>> c2 = a[[3,1,1]]
>>> a[:] = 100
>>> c1
array([1, 3])
>>> c2
array([3, 1, 1])

花式索引不返回视图的原因是,一般来说,它无法用切片来表示(如上所述,能够用偏移量、步长和计数来寻址)。

例如,花式索引可以表示为,但无法通过切片来表示。因此,返回的是包含原始数据副本的对象。

但是,花式索引似乎有时会返回视图,不是吗?

许多用户被误导,认为当他们使用这种习惯用法时,花式索引返回的是视图而不是副本。

在 [ ]
>>> a = numpy.arange(10)
>>> a[[1,2]] = 100
>>> a
array([  0, 100, 100,   3,   4,   5,   6,   7,   8,   9])

因此,似乎a<1,2>实际上是一个视图,因为元素 1 和 2 已被更新。但是,如果我们一步一步地尝试,它将无法工作。

在 [ ]
>>> a = numpy.arange(10)
>>> c1 = a[[1,2]]
>>> c1[:] = 100
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> c1
array([100, 100])

这里发生的事情是,在第一个习惯用法()中,Python 解释器将其转换为

在 [ ]
a.__setitem__([1,2], 100)

即,不需要创建视图或副本,因为该方法可以在原地进行评估(即不涉及创建新对象)。

但是,第二个习惯用法()被转换为

在 [ ]
c1 = a.__getitem__([1,2])
c1.__setitem__(slice(None, None, None), 100)  # [:] translates into slice(None, None, None)

即,在调用之前,创建一个包含副本(记住,花式索引不返回视图)的某些元素的新对象。

这里的经验法则可以是:在左值索引的上下文中(即索引位于赋值的左侧),不会创建数组的视图或副本(因为没有必要)。但是,对于常规值,上述创建视图的规则适用。

最后的练习

最后,让我们提出一个稍微高级的问题。下一个代码段按预期工作

在 [ ]
>>> a = numpy.arange(12).reshape(3,4)
>>> ifancy = [0,2]
>>> islice = slice(0,3,2)
>>> a[islice, :][:, ifancy] = 100
>>> a
array([[100,   1, 100,   3],
       [  4,   5,   6,   7],
       [100,   9, 100,  11]])

但下一个代码段不工作

在 [ ]
>>> a = numpy.arange(12).reshape(3,4)
>>> ifancy = [0,2]
>>> islice = slice(0,3,2)
>>> a[ifancy, :][:, islice] = 100  # note that ifancy and islice are interchanged here
>>> a
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

读者能发现其中的区别吗?“提示:从{{{getitem()}}} 和 {{{setitem()}}} 调用的顺序以及它们在每个示例中的作用来思考。”

章节作者:FrancescAltet、Unknown[16]、WarrenWeckesser、Ben Lewis