副本与视图#

在对 NumPy 数组进行操作时,可以直接使用视图访问内部数据缓冲区,而无需复制数据。这可以确保良好的性能,但如果用户不了解其工作原理,也可能导致不必要的问题。因此,了解这两个术语之间的区别以及哪些操作返回副本、哪些操作返回视图非常重要。

NumPy 数组是一种数据结构,由两部分组成:包含实际数据元素的连续数据缓冲区,以及包含数据缓冲区信息的元数据。元数据包括数据类型、步长(strides)和其他重要信息,这些信息有助于轻松操作ndarray。有关详细信息,请参阅NumPy 数组的内部组织一节。

视图#

可以通过仅更改某些元数据(例如步长数据类型)来以不同方式访问数组,而无需更改数据缓冲区。这创建了一种查看数据的新方式,这些新数组称为视图。数据缓冲区保持不变,因此对视图所做的任何更改都会反映在原始副本中。可以通过ndarray.view方法强制生成视图。

副本#

当通过复制数据缓冲区和元数据来创建新数组时,这称为副本。对副本所做的更改不会反映在原始数组上。创建副本较慢且占用内存,但有时是必要的。可以通过使用ndarray.copy来强制生成副本。

索引操作#

当元素可以通过原始数组中的偏移量和步长寻址时,就会创建视图。因此,基本索引操作总是创建视图。例如

>>> import numpy as np
>>> x = np.arange(10)
>>> x
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> y = x[1:3]  # creates a view
>>> y
array([1, 2])
>>> x[1:3] = [10, 11]
>>> x
array([ 0, 10, 11,  3,  4,  5,  6,  7,  8,  9])
>>> y
array([10, 11])

这里,当x改变时,y也会改变,因为它是一个视图。

另一方面,高级索引总是创建副本。例如

>>> import numpy as np
>>> x = np.arange(9).reshape(3, 3)
>>> x
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
>>> y = x[[1, 2]]
>>> y
array([[3, 4, 5],
       [6, 7, 8]])
>>> y.base is None
True

这里,y是一个副本,这由base属性表明。我们还可以通过为x[[1, 2]]赋值新值来确认这一点,而这反过来根本不会影响y

>>> x[[1, 2]] = [[10, 11, 12], [13, 14, 15]]
>>> x
array([[ 0,  1,  2],
       [10, 11, 12],
       [13, 14, 15]])
>>> y
array([[3, 4, 5],
       [6, 7, 8]])

这里必须注意,在为x[[1, 2]]赋值期间,不会创建视图或副本,因为赋值是就地(in-place)发生的。

其他操作#

numpy.reshape 函数在可能的情况下创建视图,否则创建副本。在大多数情况下,可以通过修改步长来用视图重塑数组。但是,在某些情况下,当数组变为非连续(可能在ndarray.transpose操作之后)时,无法通过修改步长来完成重塑,并且需要一个副本。在这些情况下,我们可以通过将新形状赋值给数组的 shape 属性来引发错误。例如

>>> import numpy as np
>>> x = np.ones((2, 3))
>>> y = x.T  # makes the array non-contiguous
>>> y
array([[1., 1.],
       [1., 1.],
       [1., 1.]])
>>> z = y.view()
>>> z.shape = 6
Traceback (most recent call last):
   ...
AttributeError: Incompatible shape for in-place modification. Use
`.reshape()` to make a copy with the desired shape.

以另一个操作为例,ravel 在可能的情况下返回数组的连续扁平化视图。另一方面,ndarray.flatten 总是返回数组的扁平化副本。然而,为了在大多数情况下保证视图,x.reshape(-1)可能更可取。

如何判断数组是视图还是副本#

ndarray 的base属性可以轻松判断数组是视图还是副本。视图的 base 属性返回原始数组,而副本的 base 属性返回None

>>> import numpy as np
>>> x = np.arange(9)
>>> x
array([0, 1, 2, 3, 4, 5, 6, 7, 8])
>>> y = x.reshape(3, 3)
>>> y
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
>>> y.base  # .reshape() creates a view
array([0, 1, 2, 3, 4, 5, 6, 7, 8])
>>> z = y[[2, 1]]
>>> z
array([[6, 7, 8],
       [3, 4, 5]])
>>> z.base is None  # advanced indexing creates a copy
True

请注意,base属性不应用于判断 ndarray 对象是否为新建;而仅用于判断它是否为另一个 ndarray 的视图或副本。