NumPy 数组操作基础

NumPyBeginner
立即练习

介绍

在本实验中,你将探索 NumPy 数组操作的基础概念,特别是关注副本(copies)和视图(views)之间的区别。理解这一区别对于编写高效且无 bug 的 Python 数值代码至关重要。你将编写并执行 Python 脚本来实际应用这些概念。

这是一个实验(Guided Lab),提供逐步指导来帮助你学习和实践。请仔细按照说明完成每个步骤,获得实际操作经验。根据历史数据,这是一个 初级 级别的实验,完成率为 96%。获得了学习者 97% 的好评率。

副本与视图的区别

在 NumPy 中,副本(copy)是一个拥有自己数据的全新数组,而视图(view)则是查看同一数据的另一种方式。修改视图会影响原始数组,但修改副本则不会。

让我们通过实践来了解这一点。我们将创建一个数组,然后创建它的一个视图和一个副本。接着,我们将修改它们,并观察对原始数组的影响。

首先,从左侧文件浏览器中打开 main.py 文件。然后,用以下代码替换其内容:

import numpy as np

## --- Part 1: Modifying a View ---
print("--- Modifying a View ---")
## Create an original array
original_array_view = np.array([1, 2, 3, 4, 5])
print(f"Original array: {original_array_view}")

## Create a view of the array
view_array = original_array_view.view()
## Modify the first element of the view
view_array[0] = 99
print(f"View after modification: {view_array}")
print(f"Original array after modifying the view: {original_array_view}\n")


## --- Part 2: Modifying a Copy ---
print("--- Modifying a Copy ---")
## Create another original array
original_array_copy = np.array([10, 20, 30, 40, 50])
print(f"Original array: {original_array_copy}")

## Create a copy of the array
copy_array = original_array_copy.copy()
## Modify the first element of the copy
copy_array[0] = 999
print(f"Copy after modification: {copy_array}")
print(f"Original array after modifying the copy: {original_array_copy}")

现在,保存文件并在终端中运行它以查看输出。

python main.py

你应该会看到以下输出。请注意,当视图被修改时,原始数组发生了变化,但在副本被修改时,原始数组保持不变。

--- Modifying a View ---
Original array: [1 2 3 4 5]
View after modification: [99  2  3  4  5]
Original array after modifying the view: [99  2  3  4  5]

--- Modifying a Copy ---
Original array: [10 20 30 40 50]
Copy after modification: [999  20  30  40  50]
Original array after modifying the copy: [10 20 30 40 50]

这演示了核心区别:视图与原始数据相关联,而副本则完全独立。

数组切片 - 创建视图

在 NumPy 中,一个非常常见的操作是切片(slicing),它用于从数组中选择一个元素范围。基本切片总是创建一个原始数组的视图。这是内存效率的关键特性,但你必须意识到修改切片会改变原始数据。

让我们来测试一下。清空 main.py 的内容并添加以下代码:

import numpy as np

## Create an array from 0 to 9
original_array = np.arange(10)
print(f"Original array: {original_array}")

## Create a slice of the array (elements from index 2 to 4)
array_slice = original_array[2:5]
print(f"Slice of the array: {array_slice}")

## Modify the first element of the slice
print("Modifying the first element of the slice to 100...")
array_slice[0] = 100

## Print the original array again to see the change
print(f"Original array after modification: {original_array}")

保存文件并在终端中执行它。

python main.py

你的输出将是:

Original array: [0 1 2 3 4 5 6 7 8 9]
Slice of the array: [2 3 4]
Modifying the first element of the slice to 100...
Original array after modification: [  0   1 100   3   4   5   6   7   8   9]

正如你所见,改变 array_slice[0] 也改变了 original_array[2]。这证实了切片是一个视图,而不是一个副本。

高级索引 - 创建副本

基本切片创建视图,而高级索引(advanced indexing)总是创建一个副本。高级索引涉及传递一个列表、元组或另一个索引数组来选择元素。由于选定的元素可能不在连续的内存块中,NumPy 会创建一个新数组(副本)来存储它们。

让我们将其与前一步进行对比。清空 main.py 并插入以下代码:

import numpy as np

## Create an array from 0 to 9
original_array = np.arange(10)
print(f"Original array: {original_array}")

## Use advanced indexing to select elements at indices 1, 3, and 5
indexed_array = original_array[[1, 3, 5]]
print(f"Indexed array: {indexed_array}")

## Modify the first element of the new array
print("Modifying the first element of the indexed array to 100...")
indexed_array[0] = 100

## Print the original array again to see if it changed
print(f"Original array after modification: {original_array}")

保存文件并运行脚本。

python main.py

输出将是:

Original array: [0 1 2 3 4 5 6 7 8 9]
Indexed array: [1 3 5]
Modifying the first element of the indexed array to 100...
Original array after modification: [0 1 2 3 4 5 6 7 8 9]

这次,原始数组保持不变。indexed_array 是一个副本,因此对其的修改不会影响 original_array

使用 .base 识别副本和视图

有时我们无法确定一个操作返回的是视图还是副本。NumPy 提供了一种可靠的检查方法:数组的 .base 属性。

  • 如果一个数组是视图,它的 .base 属性将指向它所属的原始数组对象。
  • 如果一个数组是副本,它的 .base 属性将是 None

让我们用这个来确认我们之前步骤中的发现。清空 main.py 并添加以下代码:

import numpy as np

## Create an original array
original_array = np.arange(10)
print(f"Original array: {original_array}\n")

## Create a view using slicing
view_slice = original_array[2:5]
print(f"Slice (view): {view_slice}")
## Check if the slice is a view of the original array
print(f"Is the slice a view? {view_slice.base is original_array}\n")

## Create a copy using advanced indexing
copy_indexed = original_array[[1, 3, 5]]
print(f"Indexed (copy): {copy_indexed}")
## Check if the indexed array is a copy
print(f"Is the indexed array a copy? {copy_indexed.base is None}")

保存文件并在终端中运行它。

python main.py

你将得到以下输出,它以编程方式确认了切片是视图,而索引数组是副本。

Original array: [0 1 2 3 4 5 6 7 8 9]

Slice (view): [2 3 4]
Is the slice a view? True

Indexed (copy): [1 3 5]
Is the indexed array a copy? True

.base 属性是调试和确保你的代码按预期行为的宝贵工具。

总结

在本实验中,你学习了 NumPy 数组的副本(copy)和视图(view)之间的关键区别。你练习了创建两者,并观察了修改如何影响原始数据。

  • 你了解到 .view() 和基本切片等方法会创建视图,它们内存效率高,但修改时可能导致意外的副作用。
  • 你了解到 .copy() 和高级索引等方法会创建副本,它们与原始数组是独立的。
  • 最后,你学习了如何使用 .base 属性来明确检查一个数组是视图还是副本。

掌握这些概念是使用 NumPy 编写健壮高效的数值应用程序的关键一步。