Skip to content

NumPy - 复制与视图

NumPy - 副本(Copies)与视图(Views)

Section titled “NumPy - 副本(Copies)与视图(Views)”

在使用 NumPy 数组时,某些操作会返回原始数组的“副本”,而另一些则返回“视图”。理解它们之间的区别至关重要,这有助于编写高效且无 bug 的代码。

**副本(Copy)**意味着数据在另一个内存位置被物理复制。对副本的更改不会影响原始数组,反之亦然。

另一方面,视图(View)提供了对相同内存内容的不同视角。通过视图进行的更改会影响原始数组,并且对原始数组的更改可能会影响视图。可以将视图视为查看相同数据的不同镜头。

简单的赋值操作(b = a)不会创建数组或其数据的副本。它使得两个变量引用完全相同的内存中的 NumPy ndarray 对象。您可以使用 Python 内置的 id() 函数来验证这一点,该函数返回对象的唯一标识符(类似于内存地址)。

由于两个变量指向同一个对象,通过一个变量进行的任何修改(例如改变形状或元素)都将立即反映在使用另一个变量访问数组时。

import numpy as np
a = np.arange(6)
print(f"Original array 'a':\n{a}")
print(f"ID of 'a': {id(a)}\n") # ID 会有所不同
print("Assigning 'b = a'")
b = a
print(f"Array 'b':\n{b}")
print(f"ID of 'b': {id(b)} (Same as 'a')\n") # 与 'a' 相同
print("Changing shape of 'b' to (3, 2)")
b.shape = (3, 2)
print(f"New shape of 'b':\n{b}\n") # 'b' 的新形状
print("Shape of 'a' also changes:") # 'a' 的形状也随之改变
print(a)

输出:

Original array 'a':
[0 1 2 3 4 5]
ID of 'a': 140189398520688 # ID will vary
Assigning 'b = a'
Array 'b':
[0 1 2 3 4 5]
ID of 'b': 140189398520688 (Same as 'a')
Changing shape of 'b' to (3, 2)
New shape of 'b':
[[0 1]
[2 3]
[4 5]]
Shape of 'a' also changes:
[[0 1]
[2 3]
[4 5]]

视图创建一个新的 ndarray 对象,但它与原始数组共享底层数据缓冲区。NumPy 的 ndarray.view() 方法显式地创建一个视图。

视图的关键特性:

  • 视图有自己的对象标识(id(a) != id(b))。
  • 视图与原始数组共享数据。在视图中修改元素会改变原始数组,反之亦然。
  • 视图可以具有不同的形状(shape)、跨度(strides)和数据类型(如果兼容),而不会影响原始数组的形状或 dtype 属性。但是,它们仍然指向相同的内存。

获得视图的一种非常常见的方式是通过切片(slicing)。

import numpy as np
# 从一个 3x2 的数组开始
a = np.arange(6).reshape(3, 2)
print(f"Original array 'a':\n{a}")
print(f"ID of 'a': {id(a)}\n") # ID 会有所不同
print("Creating view 'b = a.view()'")
b = a.view()
print(f"View 'b':\n{b}")
print(f"ID of 'b': {id(b)} (Different from 'a')\n") # 与 'a' 不同
# 改变视图 'b' 的形状。这不会改变 'a'.shape。
# Change the shape of the view 'b'. This does NOT change 'a'.shape.
b.shape = (2, 3)
print(f"Shape of 'b' changed:\n{b}") # 'b' 的形状已改变
print(f"Shape of 'a' remains unchanged:\n{a}\n") # 'a' 的形状保持不变
# 修改视图 'b' 中的一个元素
# Modify an element in the view 'b'
print("Modifying b[0, 0] = 99") # 修改 b[0, 0] = 99
b[0, 0] = 99
print(f"View 'b' after modification:\n{b}") # 修改后的视图 'b'
print(f"Original array 'a' is also affected:\n{a}") # 原始数组 'a' 也受到了影响

输出:

Original array 'a':
[[0 1]
[2 3]
[4 5]]
ID of 'a': 140189398521008 # ID will vary
Creating view 'b = a.view()'
View 'b':
[[0 1]
[2 3]
[4 5]]
ID of 'b': 140189398521152 # ID will vary (Different from 'a')
Shape of 'b' changed:
[[0 1 2]
[3 4 5]]
Shape of 'a' remains unchanged:
[[0 1]
[2 3]
[4 5]]
Modifying b[0, 0] = 99
View 'b' after modification:
[[99 1 2]
[ 3 4 5]]
Original array 'a' is also affected:
[[99 1]
[ 2 3]
[ 4 5]]

基本的切片操作在 NumPy 中通常返回视图。这是一种性能优化,但如果您期望获得一个副本,可能会感到意外。

import numpy as np
a = np.array([[10, 20, 30],
[40, 50, 60],
[70, 80, 90]])
print(f"Original array 'a':\n{a}\n")
print("Create a slice 's = a[:2, 1:]' (first 2 rows, columns from index 1 onwards)") # 创建切片 's = a[:2, 1:]'(前两行,从索引 1 开始的列)
s = a[:2, 1:] # 选中 [[20, 30], [50, 60]]
print(f"Slice 's':\n{s}\n")
print("Modify s[0, 0] = 999") # 修改 s[0, 0] = 999
s[0, 0] = 999 # 修改切片的第一个元素
print(f"Slice 's' after modification:\n{s}") # 修改后的切片 's'
print(f"Original array 'a' is also modified:\n{a}") # 原始数组 'a' 也受到了影响

输出:

Original array 'a':
[[ 10 20 30]
[ 40 50 60]
[ 70 80 90]]
Create a slice 's = a[:2, 1:]' (first 2 rows, columns from index 1 onwards)
Slice 's':
[[20 30]
[50 60]]
Modify s[0, 0] = 999
Slice 's' after modification:
[[999 30]
[ 50 60]]
Original array 'a' is also modified:
[[ 10 999 30]
[ 40 50 60]
[ 70 80 90]]

深拷贝,使用 ndarray.copy() 方法创建,它会复制数组对象及其数据。新数组与原始数组完全独立。

当您需要修改一个新数组而不影响原始数组时,请使用深拷贝。

import numpy as np
a = np.array([[10, 10],
[ 2, 3],
[ 4, 5]])
print(f"Original array 'a':\n{a}")
print(f"ID of 'a': {id(a)}\n") # ID 会有所不同
print("Create a deep copy 'b = a.copy()'") # 创建一个深拷贝 'b = a.copy()'
b = a.copy()
print(f"Array 'b':\n{b}")
print(f"ID of 'b': {id(b)} (Different from 'a')\n") # 与 'a' 不同
# 检查 'b' 是否与 'a' 共享内存
# Check if 'b' shares memory with 'a'
# np.shares_memory() 是更直接的检查方法
# np.shares_memory() is a more direct way to check this
print(f"Does 'b' share memory with 'a'? {np.shares_memory(a, b)}")
# 'is' 运算符检查对象标识,对于副本来说会是 False。
# The 'is' operator checks object identity, which will be False for a copy.
print(f"Is 'b' the same object as 'a'? {b is a}\n")
print("Change the contents of 'b': b[0, 0] = 100") # 改变 'b' 的内容: b[0, 0] = 100
b[0, 0] = 100
print(f"Modified array 'b':\n{b}\n") # 修改后的数组 'b'
print(f"Array 'a' remains unchanged:\n{a}") # 数组 'a' 保持不变

输出:

Original array 'a':
[[10 10]
[ 2 3]
[ 4 5]]
ID of 'a': 140189398521568 # ID will vary
Create a deep copy 'b = a.copy()'
Array 'b':
[[10 10]
[ 2 3]
[ 4 5]]
ID of 'b': 140189398521712 # ID will vary (Different from 'a')
Does 'b' share memory with 'a'? False
Is 'b' the same object as 'a'? False
Change the contents of 'b': b[0, 0] = 100
Modified array 'b':
[[100 10]
[ 2 3]
[ 4 5]]
Array 'a' remains unchanged:
[[10 10]
[ 2 3]
[ 4 5]]

**关键点:**在使用数组时,请注意操作返回的是视图还是副本,尤其是在修改数组时。如果您需要独立性,请明确使用 .copy()。