NumPy - 副本与视图
在 NumPy 中,对数组执行操作时,结果可能是原始数据的副本,也可能只是原始数据的视图。了解两者之间的区别对于高效的内存管理和避免代码中出现意外的副作用至关重要。
在 NumPy 中创建副本
我们可以使用 copy() 函数在 NumPy 中显式创建数组的副本。此函数会生成一个新数组,并将原始数组中的数据复制到这个新数组中。
在 NumPy 中创建数组副本时,数据会被完全复制。这意味着对副本所做的更改不会影响原始数组,反之亦然。当您需要处理数组的修改版本而不更改原始数据时,副本非常有用。
示例
在以下示例中,修改 copied_array 不会影响 original_array,从而证明了两个数组的独立性 -
import numpy as np # 原始数组 original_array = np.array([1, 2, 3]) # 创建副本 copied_array = original_array.copy() # 修改副本 copied_array[0] = 10 print("原始数组:", original_array) print("复制的数组:", duplicated_array)
以下是获得的输出−
原始数组:[1 2 3] 复制后的数组:[10 2 3]
浅拷贝 vs. 深拷贝
在 NumPy 数组的语境中,浅拷贝和深拷贝之间的区别对于理解数据在复制时的处理方式至关重要。
浅拷贝
数组的浅拷贝会创建一个新的数组对象,但如果原始数组中的元素本身是数组或其他复杂对象,则不会创建这些元素的副本。
相反,新数组仍然引用与原始数组相同的元素。这意味着对元素内容的更改将同时影响原始数组和复制的数组。
- 数组级复制 − 对于 NumPy 数组,浅拷贝意味着虽然复制了顶层数组对象,但不会复制底层数据缓冲区。新数组只是相同数据的新视图。
- 用法 - 当您需要一个新的数组对象,但又想避免复制大量数据的开销时,浅拷贝非常有用。
示例
在此示例中,修改 shallow_copy 也会修改 original_array,因为它们共享相同的底层数据 -
import numpy as np # 原始数组 original_array = np.array([[1, 2, 3], [4, 5, 6]]) # 浅拷贝 shallow_copy = original_array.view() # 修改浅拷贝中的元素 shallow_copy[0, 0] = 100 print("原始数组:") print(original_array) print(" 浅拷贝:") print(shallow_copy)
这将产生以下结果 -
原始数组: [[100 2 3] [ 4 5 6]] 浅拷贝: [[100 2 3] [ 4 5 6]]
深拷贝
另一方面,深拷贝会创建一个新的数组对象,并复制其包含的所有数据。这意味着对新数组的任何更改都不会影响原始数组,反之亦然。新数组中的数据与原始数组中的数据完全独立。
- 完全复制 − 在 NumPy 中,深层复制涉及复制数组的整个数据缓冲区,以确保新数组与原始数组完全独立。
- 用法 − 当你需要独立于原始数组处理数据时,深层复制非常重要,尤其是在修改数据时不会影响原始数组。
示例
在本例中,修改 deep_copy 不会影响 original_array,从而证明了两个数组的独立性 −
import numpy as np # 原始数组 original_array = np.array([[1, 2, 3], [4, 5, 6]]) # 深层复制 deep_copy = original_array.copy() # 修改深层复制中的元素 deep_copy[0, 0] = 100 print("原始数组:") print(original_array) print(" 深层复制:") print(deep_copy)
以下是上述代码的输出 -
原始数组: [[1 2 3] [4 5 6]] 深层复制: [[100 2 3] [ 4 5 6]]
复制子数组
为了避免在处理子数组时修改原始数组,您应该创建子数组。当您需要独立于原始数据操作或分析子数组时,这非常有用。
子数组只是现有 NumPy 数组的一部分。您可以使用切片技术提取子数组。
例如,如果您有一个二维数组,您可以通过沿其行和列进行切片来提取一个较小的二维子数组。但是,默认情况下,切片会创建原始数组的视图,而不是单独的副本。这意味着,除非您明确创建副本,否则对子数组的更改也会影响原始数组。
示例
在下面的示例中,由于使用了 copy() 函数,sub_array 是一个完全独立的数组 -
import numpy as np # 原始二维数组 original_array = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) # 创建子数组的副本 sub_array = original_array[0:2].copy() sub_array[0] = 20 print("复制子数组后的原始数组:", original_array) print("子数组:", sub_array)
得到的输出如下所示 -
复制子数组后的原始数组: [[1 2 3] [4 5 6] [7 8 9]] 子数组: [[20 20 20] [ 4 5 6]]
在 NumPy 中创建视图
视图是在对数组进行切片或执行某些操作(例如重塑)时创建的。数据不会被复制;相反,新数组只是查看原始数据的另一种方式。
换句话说,视图是一个新的数组对象,它查看与原始数组相同的数据。这意味着,如果您修改视图,更改将反映在原始数组中,反之亦然。
示例
在此示例中,修改 view_array 会直接影响 original_array,表明它们共享相同的数据 -
import numpy as np # 原始数组 original_array = np.array([1, 2, 3]) view_array = original_array[0:2] # 修改视图 view_array[0] = 30 print("视图修改后的原始数组:", original_array) print("视图数组:", view_array)
执行上述代码后,我们得到以下输出 -
视图修改后的原始数组:[30 2 3] 视图数组:[30 2]
何时返回视图?
并非所有切片或操作都会生成视图。如果数组的内存布局发生变化,NumPy 可能会返回副本而不是视图。
切片生成的视图
NumPy 中返回视图的最常见场景是对数组进行切片。切片是一种通过指定索引范围来提取数组一部分的方法。NumPy 不会创建一个包含自身数据的新数组,而是返回一个视图,这意味着切片后的数组与原始数组共享相同的数据。
示例
在此示例中,view_array 是 original_array 的视图。数据不会被复制,并且两个数组共享相同的底层内存。这意味着对"view_array"所做的任何更改都会影响"original_array" -
import numpy as np # 原始数组 original_array = np.array([1, 2, 3, 4, 5]) # 通过切片原始数组创建视图 view_array = original_array[1:4] print("原始数组:") print(original_array) print(" 切片后的视图数组:") print(view_array)
结果如下 -
原始数组: [1 2 3 4 5] 切片后的视图数组: [2 3 4]
来自重塑
另一种返回视图的常见场景是重塑数组。重塑会改变数组的形状(即每个维度的元素数量),而不会更改底层数据。如果可能,NumPy 会返回原始数组的新形状视图。
示例
此处,reshaped_array 是 original_array 的视图,以"2x3"格式简单呈现。数据保持不变,修改"reshaped_array"也会修改"original_array" -
import numpy as np # 原始一维数组 original_array = np.array([1, 2, 3, 4, 5, 6]) # 将数组重塑为 2x3 矩阵 reshaped_array = original_array.reshape(2, 3) print("原始数组:") print(original_array) print(" 重塑后的数组(视图):") print(reshaped_array)
我们得到如下所示的输出 -
原始数组:[1 2 3 4 5 6] 重塑后的数组(视图): [[1 2 3] [4 5 6]]
转置后的视图
转置数组涉及沿对角线翻转,将行转换为列,反之亦然。使用 np.transpose() 等函数或 .T 属性转置数组时,NumPy 会尽可能返回视图,而不是副本。
示例
在本例中,transposed_array 是 original_array 的视图,但轴方向已互换。底层数据保持不变,对"transposed_array"的更改将反映在"original_array"中 -
import numpy as np # 原始二维数组 original_array = np.array([[1, 2, 3], [4, 5, 6]]) # 转置数组 transposed_array = original_array.T print("原始数组:") print(original_array) print(" 转置数组(视图):") print(transposed_array)
以下是获得的输出 -
原始数组: [[1 2 3] [4 5 6]] 转置数组(视图): [[1 4] [2 5] [3 6]]
Base 属性
在 NumPy 中,数组的 base 属性用于检查该数组是另一个数组的视图还是副本。它是对派生当前数组的原始数组的引用。
如果当前数组是另一个数组的视图,则 "base" 将指向该原始数组。如果当前数组不是视图(即,它是原始数组或深层副本),则 "base" 将为 None。
示例:原始数组的 base 属性
创建数组时,其 base 属性将为 None,因为它是原始数组 -
import numpy as np # 创建原始数组 original_array = np.array([10, 20, 30, 40, 50]) # 检查 base 属性 print("原始数组的 Base:", original_array.base)
这将产生以下结果 -
原始数组的 Base: None
示例:视图的 base 属性
创建数组的视图(例如,通过切片)时,视图的 base 属性将指向原始数组 -
import numpy as np # 创建原始数组 original_array = np.array([10, 20, 30, 40, 50]) # 创建原始数组的视图 view_array = original_array[1:4] # 检查 base 属性 print("视图数组的 Base:", view_array.base)
以下是上述代码的输出 -
视图数组的 Base: [10 20 30 40 50]
示例:副本的 base 属性
如果创建数组的副本,则 base 属性将为 None,表示复制的数组与原始数组无关 -
import numpy as np # 创建原始数组 original_array = np.array([10, 20, 30, 40, 50]) # 创建原始数组的副本 copy_array = original_array.copy() # 检查 base 属性 print("复制数组的 Base:", copy_array.base)
以下是上述代码的输出 -
复制数组的 Base: None