拷贝与视图#

在操作 NumPy 数组时,可以通过视图(view)直接访问内部数据缓冲区,而无需复制数据。这确保了良好的性能,但如果不了解其工作原理,也可能导致一些不希望出现的问题。因此,了解这两个术语之间的区别,并知道哪些操作返回拷贝,哪些返回视图,是非常重要的。

NumPy 数组是一种由两部分组成的数据结构: 1. 包含实际数据元素的 contiguous 数据缓冲区, 2. 包含数据缓冲区相关信息的元数据。元数据包括数据类型、步长(strides)等重要信息,这些信息有助于轻松操纵 ndarray

有关详细信息,请参阅 Internal organization of NumPy arrays 部分。

视图#

通过仅更改某些元数据(如 stridedtype)而不更改数据缓冲区,可以以不同的方式访问数组。这创建了一种新的数据查看方式,这些新的数组称为视图。数据缓冲区保持不变,因此对视图所做的任何更改都会反映在原始副本中。可以通过调用方法 .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]  # 创建视图
>>> 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 也会发生变化,因为 yx 的视图。

另一方面,高级索引 始终创建副本。例如:

>>> 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]] 进行赋值时,并没有创建视图或副本,而是直接在原数组上进行修改。

其他操作#

numpy.reshape() 函数在可能的情况下创建视图,否则创建副本。在大多数情况下,可以通过修改步长来重新塑造数组以创建视图。然而,在某些情况下,数组变得不连续(例如在 transpose() 操作之后),重新塑造数组无法通过修改步长来实现,因此需要创建副本。在这种情况下,可以通过将新形状赋值给数组的形状属性来引发错误。例如:

>>> import numpy as np
>>> x = np.ones((2, 3))
>>> y = x.T  # 使数组不连续
>>> y
array([[1., 1.],
       [1., 1.],
       [1., 1.]])
>>> z = y.view()
>>> z.shape = 6
Traceback (most recent call last):
   ...
AttributeError: 无法进行原地修改,形状不兼容。使用 `.reshape()` 创建具有所需形状的副本。

在另一个操作示例中,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() 创建了一个视图
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  # 高级索引创建了一个副本
True

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