NumPy:数组复制与视图
在使用 NumPy 进行数据处理时,数组对象不仅可以被读取或修改,还经常需要在不同变量或不同数组之间进行“复制”。例如:将一个数组赋值给另一个变量、通过切片获取数组的一部分、或显式创建新的数组副本。
需要注意的是,这些操作在语法上都表现为“复制”,但在内存层面的行为却可能完全不同。
NumPy 数组由数据缓冲区(data buffer)与数组结构信息(shape、strides、dtype 等)两部分构成。不同“复制方式”的本质差异,在于这两部分是否被共享。
从内存共享的角度来看,NumPy 中数组之间的关系可以分为三类:
(1)引用(reference)
(2)视图(view)
(3)副本(copy)
理解这三种关系,是掌握 NumPy 数组行为的重要基础。
一、数组引用(Reference)
在 Python 中,如果将一个数组赋值给另一个变量:
b = a此时并不会创建新的数组对象,而只是创建了一个新的变量名指向同一个数组。
示例:
import numpy as np a = np.array([[1, 2, 3], [4, 5, 6]]) b = ab[0, 0] = 99 print(a)输出:
[[99 2 3] [ 4 5 6]]说明:
该行为本质上属于引用(别名)绑定,而非拷贝。
变量 a 与 b 指向的是同一个数组对象,因此修改任意一方都会影响另一方。
可以通过 id() 函数查看对象标识:
print(id(a))print(id(b))两个变量对应的对象标识是相同的。
二、数组视图(View)
NumPy 提供了一种重要机制:视图(view)。
视图是一个新的数组对象,它与原数组共享底层数据缓冲区。即:
• 数据共享
• 数组结构独立(shape、strides 通常独立)
因此:
• 修改视图的数据,会影响原数组的数据
• 修改视图的结构,通常不影响到原数组
需要注意的是,虽然视图通常具有独立的结构信息,但在某些操作中(如直接修改属性),仍需谨慎处理结构一致性。
这种设计的目的在于:
• 提高内存利用率
• 避免不必要的数据复制
• 提升大规模数组计算效率
1、创建视图的常用方法
view()
创建当前数组的数据视图,新数组对象与原数组共享底层数据。
ndarray.view(dtype=None, type=None)参数说明:
• dtype:可选,指定新的数据类型
• type:可选,指定返回数组的子类
返回值:
返回一个新的 ndarray 对象(共享数据)。
示例:
a = np.array([[1, 2, 3], [4, 5, 6]]) b = a.view()b[0, 0] = 100 print(a)输出:
[[100 2 3] [ 4 5 6]]说明:
虽然 a 与 b 是不同的数组对象,但它们共享同一块数据内存,因此修改其中一个数组会影响另一个数组。
2、返回视图的常见操作
(1)切片
在 NumPy 中,基本索引(切片)通常返回视图。
示例:
a = np.array([[1, 2, 3], [4, 5, 6]]) b = a[:, 1:3]b[0, 0] = 99 print(a)输出:
[[ 1 99 3] [ 4 5 6]]说明:
数组 b 是 a 的切片视图,因此修改 b 的数据会影响原数组。
(2)数组形状重构时
在不改变数据内容的情况下,使用 reshape() 重新解释数组的形状。多数情况下返回视图。
示例:
a = np.arange(6)b = a.reshape(2, 3) b[0, 0] = 100print(a)输出:
[100 1 2 3 4 5]说明:
reshape() 在大多数情况下返回视图,因此 b 与 a 共享数据内存。修改 b 会反映到原数组 a 上。
(3)展平数组时
使用 numpy.ravel() 将数组展平为一维数组时,优先返回视图。
示例:
a = np.array([[1, 2, 3], [4, 5, 6]]) b = np.ravel(a)b[0] = 100print(a)输出:
[[100 2 3] [ 4 5 6]]三、数组副本(Copy)
如果希望获得一个完全独立的数组,则需要创建数组副本。
1、显式创建数组副本
NumPy 提供了 ndarray.copy() 和 numpy.copy() 两种复制方法。
ndarray.copy()
创建数组的完整副本,复制底层数据,新数组与原数组的数据完全独立。
ndarray.copy(order='C')参数说明:
• order:内存布局方式(默认为 'C')
返回值:
返回一个新的独立的 ndarray 数组对象。
示例:
a = np.array([[1, 2, 3], [4, 5, 6]]) b = a.copy()b[0, 0] = 99 print(a)输出:
[[1 2 3] [4 5 6]]说明:
数组 b 是 a 的数据副本,因此修改 b 不会影响 a。
numpy.copy()
返回输入数组的副本。
numpy.copy(a)参数说明:
• a:输入数组
返回值:
返回一个新的独立的 ndarray 数组对象。
示例:
a = np.array([[1, 2, 3], [4, 5, 6]]) b = np.copy(a)b[0, 0] = 99 print(a)输出:
[[1 2 3] [4 5 6]]说明:
函数 numpy.copy() 与 ndarray.copy() 的行为基本一致。
2、返回副本的常见操作
(1)高级索引时
当使用整数数组或布尔数组进行高级索引时,返回副本,不共享底层数据。
示例:
a = np.array([[1, 2, 3], [4, 5, 6]]) b = a[:, [1, 2]]b[0, 0] = 99 print(a)输出:
[[1 2 3] [4 5 6]]说明:
虽然语法类似切片,但因使用整数数组索引,返回副本。
需要注意的是,是否返回视图,并不取决于语法形式,而取决于索引类型。只要使用整数数组或布尔数组,即属于高级索引,将返回副本。
(2)展平数组时
使用 ndarray.flatten() 将数组展平为一维数组时,始终复制数据。
示例:
a = np.array([[1, 2, 3], [4, 5, 6]]) b = a.flatten()b[0] = 100print(a)输出:
[[1 2 3] [4 5 6]]说明:
flatten() 会返回数组副本,因此修改 b 不会影响原数组 a。
四、浅拷贝与深拷贝
在 Python 中,对象复制通常分为浅拷贝(shallow copy)和深拷贝(deep copy)两种方式,通常通过 copy 模块实现:
copy.copy()copy.deepcopy()但对于 NumPy 数组来说,情况略有不同。
由于 NumPy 数组的数据通常存储在连续的内存块 中,因此 ndarray.copy() 通常已经复制了数组数据,在大多数情况下,它的行为相当于 深拷贝。
例如:
import numpy as np a = np.array([1,2,3])b = a.copy()此时两个数组的数据完全独立。
需要注意的是,如果数组的元素类型 dtype 为 object,则数组中存储的是 Python 对象引用。在这种情况下,ndarray.copy() 只会复制数组结构,而不会递归复制对象本身,因此仍可能出现浅拷贝行为。
五、判断是否共享内存
NumPy 提供了两个函数,用于判断数组之间是否共享内存。
numpy.shares_memory()
判断两个数组是否共享同一块内存。
numpy.shares_memory(a, b)返回值:
返回布尔值。
说明:
numpy.may_share_memory() 与其基本一致,但基于保守判断,判断两个数组是否可能共享内存。
示例:
a = np.arange(6).reshape(2, 3)b = a[:, 1:] print(np.shares_memory(a, b))输出:
True说明:
数组 b 是 a 的切片视图,因此两者共享数据内存。
如果数组是通过 copy() 创建:
c = a.copy()np.shares_memory(a,c)输出:
False说明两个数组的数据不共享内存,彼此独立。
📘 小结
NumPy 中的“复制”涉及数据缓冲区与数组结构两个层面。变量赋值仅创建引用;切片与 view 通常生成共享数据的视图;而 copy() 等操作才会复制底层数据。理解这些机制,有助于避免隐式数据修改,并提升数组计算的效率。
“点赞有美意,赞赏是鼓励”
