NumPy: 索引与切片
1. 你将学到
- ❶ 1D 索引与切片:一维数组的下标访问与区间截取
- ❷ 2D 索引:行、列与子矩阵的提取
- ❸ 3D 索引直觉:高维数组的逐层降维思维
- ❹ 步长切片:正步长、负步长与负索引的组合
- ❺ 切片是视图:理解视图与副本的本质区别
2. 故事
Charlie 正在分析一份 10K 行的传感器数据矩阵。他用 data[0:3] 取出前 3 行做异常值清洗,顺手将缺失值设为 -1:
subset = data[0:3]
subset[subset == -999] = -1
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
清洗完打印 subset,数据很漂亮。但当他回头查看 data 时,发现原始数据的前 3 行也被改了!
"切片返回的是视图,不是副本!" Alice 一语道破。
她教 Charlie 用 subset = data[0:3].copy() 才能安全操作,不影响原始数组。Charlie 深刻体会到:NumPy 切片默认共享内存,这是高效之源,也是陷阱之门。
3. 知识点
(1) 基本索引
一维数组的索引与 Python 列表一致:从 0 开始,负索引从 -1 倒数。
import numpy as np
a = np.array([10, 20, 30, 40, 50])
a[0] # 10
a[-1] # 50
a[2] # 30
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) 切片语法
切片格式为 start:stop:step,三个部分均可省略:
| 省略形式 | 等价写法 | 含义 |
|---|---|---|
a[:] |
a[0:len(a):1] |
全部元素 |
a[2:] |
a[2:len(a):1] |
从第 3 个到末尾 |
a[:3] |
a[0:3:1] |
前 3 个元素 |
a[::2] |
a[0:len(a):2] |
隔一个取一个 |
a[::-1] |
a[len(a)-1::-1] |
整体反转 |
a = np.arange(10) # [0 1 2 3 4 5 6 7 8 9]
a[2:7] # [2 3 4 5 6]
a[::3] # [0 3 6 9]
a[::-1] # [9 8 7 6 5 4 3 2 1 0]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(3) 负索引
负索引从数组末尾计数,-1 是最后一个元素,-2 是倒数第二个,以此类推。
a = np.array([10, 20, 30, 40, 50])
a[-1] # 50
a[-2] # 40
a[-3:] # [30 40 50]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(4) 多维切片
二维数组的索引为 a[row, col],切片在每一维独立应用:
b = np.arange(12).reshape(3, 4)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
b[1, 2] # 6 (row 1, col 2)
b[0, :] # [0 1 2 3] (entire row 0)
b[:, 1] # [1 5 9] (entire col 1)
b[0:2, 1:3] # [[1 2], [5 6]] (2x2 sub-matrix)
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
三维数组在二维基础上再增加一个维度(深度/通道),索引为 a[depth, row, col]:
c = np.arange(24).reshape(2, 3, 4)
# c[0] is the first 3x4 plane
# c[1] is the second 3x4 plane
c[0, 1, 2] # 6 (depth 0, row 1, col 2)
c[:, 0:2, :] # shape (2, 2, 4) - first 2 rows of each plane
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(5) 省略号
当数组维度很高时,...(省略号)可以代替连续的 :,使代码更简洁:
c = np.arange(24).reshape(2, 3, 4)
c[..., 0] # equivalent to c[:, :, 0] -> shape (2, 3)
c[0, ...] # equivalent to c[0, :, :] -> shape (3, 4)
c[0, ..., 1] # equivalent to c[0, :, 1] -> shape (3,)
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
省略号只能出现一次,且会自动填充到缺失的所有维度的完整切片。
(6) 视图 vs 副本
这是 NumPy 切片最重要的特性:切片返回视图(view),索引返回标量或副本。
| 操作 | 返回类型 | 修改是否影响原数组 |
|---|---|---|
a[1:4] |
视图 | 是 |
a[0] |
标量 | 否(标量不可变) |
a[[0, 2]] |
副本 | 否(花式索引返回副本) |
a[a > 0] |
副本 | 否(布尔索引返回副本) |
a[1:4].copy() |
副本 | 否 |
a = np.arange(6) # [0 1 2 3 4 5]
s = a[2:5] # view: [2 3 4]
s[:] = 100
# a is now [ 0 1 100 100 100 5]
b = a[2:5].copy() # copy: independent
b[:] = 0
# a unchanged
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(7) 切片赋值
切片是视图,因此对切片赋值会直接修改原数组。这是高效操作大数组的利器:
a = np.zeros(10)
a[3:7] = 99
# a -> [ 0. 0. 0. 99. 99. 99. 99. 0. 0. 0.]
a[::2] = -1
# a -> [-1. 0. -1. 99. -1. 99. -1. 0. -1. 0.]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(2) 切片内存模型
graph LR
A["Original array a<br/>[0, 1, 2, 3, 4, 5]"] -->|"a[2:5] creates view"| B["View s<br/>[2, 3, 4]"]
B -->|"s shares memory with a"| A
B -->|"s[:] = 100"| C["Modified a<br/>[0, 1, 100, 100, 100, 5]"]
A -.->|".copy() creates independent copy"| D["Copy b<br/>[2, 3, 4]"]
D -->|"b[:] = 0"| E["Copy b<br/>[0, 0, 0]"]
E -.->|"No effect on a"| A
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
(3) 对比:list 切片 vs NumPy 切片
| 特性 | Python list | NumPy ndarray |
|---|---|---|
a[1:4] 返回 |
新列表(副本) | 视图(共享内存) |
| 修改切片 | 不影响原列表 | 影响原数组 |
| 获取副本 | 默认就是 | 需 .copy() |
| 多维索引 | 不支持 | a[row, col] |
| 省略号 | 不支持 | a[..., 0] |
| 步长切片 | 支持 | 支持 |
| 切片赋值 | 不支持 | 支持 |
(4) 对比:1D / 2D / 3D 索引语法
| 维度 | 索引取值 | 取一行/一列 | 子区域切片 |
|---|---|---|---|
| 1D | a[i] |
— | a[2:5] |
| 2D | a[i, j] |
a[i, :] / a[:, j] |
a[0:2, 1:3] |
| 3D | a[d, i, j] |
a[d, i, :] / a[d, :, j] |
a[0:1, 0:2, 1:3] |
(5) 对比:视图 vs 副本
| 判断方式 | 视图(共享内存) | 副本(独立内存) |
|---|---|---|
np.shares_memory(a, s) |
True |
False |
s.base is a |
True |
False |
修改 s 后 a 变化 |
是 | 否 |
| 内存开销 | 极小 | 与原数组相当 |
(6) 对比:常见切片模式
| 模式 | 语法 | 含义 |
|---|---|---|
| 前n个 | a[:n] |
索引 0 到 n-1 |
| 后n个 | a[-n:] |
最后 n 个元素 |
| 去首尾 | a[1:-1] |
去掉第1个和最后1个 |
| 偶数位置 | a[::2] |
步长2从头开始 |
| 奇数位置 | a[1::2] |
步长2从索引1开始 |
| 反转 | a[::-1] |
步长-1,整体翻转 |
| 隔2取1 | a[::3] |
步长3 |
4. 示例
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:1D 索引与切片(难度⭐)
import numpy as np
a = np.arange(10, 20) # [10 11 12 13 14 15 16 17 18 19]
# Basic indexing
print(a[0]) # 10
print(a[-1]) # 19
print(a[5]) # 15
# Slicing
print(a[2:6]) # [12 13 14 15]
print(a[:4]) # [10 11 12 13]
print(a[7:]) # [17 18 19]
print(a[::2]) # [10 12 14 16 18]
print(a[::-1]) # [19 18 17 16 15 14 13 12 11 10]
# Negative index slicing
print(a[-3:]) # [17 18 19]
print(a[-5:-2])# [15 16 17]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:2D 行列与子矩阵(难度⭐⭐)
import numpy as np
b = np.arange(20).reshape(4, 5)
# [[ 0 1 2 3 4]
# [ 5 6 7 8 9]
# [10 11 12 13 14]
# [15 16 17 18 19]]
# Single element
print(b[1, 2]) # 7
# Entire row
print(b[2, :]) # [10 11 12 13 14]
print(b[2]) # same result, row index alone
# Entire column
print(b[:, 3]) # [ 3 8 13 18]
# Sub-matrix (rows 1-2, cols 2-4)
print(b[1:3, 2:5])
# [[ 7 8 9]
# [12 13 14]]
# First and last rows
print(b[[0, -1], :])
# [[ 0 1 2 3 4]
# [15 16 17 18 19]]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:步长切片与负步长(难度⭐⭐)
import numpy as np
a = np.arange(15) # [0 1 2 ... 14]
# Step slicing
print(a[1:12:3]) # [1 4 7 10] start=1, step=3
print(a[::4]) # [0 4 8 12] start=0, step=4
# Negative step: traverses from end to start
print(a[::-1]) # [14 13 12 ... 1 0]
print(a[::-2]) # [14 12 10 8 6 4 2 0]
# Negative step with start/stop
print(a[12:3:-2]) # [12 10 8 6 4] from index 12 down to 4 (stop=3 excluded)
print(a[-1:5:-3]) # [14 11 8] from last to index 6 (stop=5 excluded)
# 2D step slicing
b = np.arange(20).reshape(4, 5)
print(b[::2, ::2])
# [[ 0 2 4]
# [10 12 14]]
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:切片赋值与视图验证(难度⭐⭐)
import numpy as np
# View: slice shares memory
a = np.arange(8) # [0 1 2 3 4 5 6 7]
s = a[2:5] # view -> [2 3 4]
print(np.shares_memory(a, s)) # True
print(s.base is a) # True
# Modify view -> original changes
s[:] = 99
print(a) # [ 0 1 99 99 99 5 6 7]
# Slice assignment to original
a[4:7] = -1
print(a) # [ 0 1 99 99 -1 -1 -1 7]
# Copy: independent
b = a[2:5].copy()
print(np.shares_memory(a, b)) # False
b[:] = 0
print(a) # [ 0 1 99 99 -1 -1 -1 7] unchanged
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:省略号索引(难度⭐⭐)
import numpy as np
c = np.arange(24).reshape(2, 3, 4)
# Shape: (2, 3, 4) -> depth, rows, cols
# Ellipsis replaces all middle dimensions
print(c[..., 0]) # shape (2, 3), same as c[:, :, 0]
# [[ 0 4 8]
# [12 16 20]]
print(c[0, ...]) # shape (3, 4), same as c[0, :, :]
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
print(c[0, ..., 1]) # shape (3,), same as c[0, :, 1]
# [1 5 9]
# Compare explicit vs ellipsis
print(np.all(c[..., 2] == c[:, :, 2])) # True
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
▶ 示例
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
:综合——5×5 棋盘矩阵四象限/对角线/视图验证(难度⭐⭐⭐)
import numpy as np
# Build a 5x5 checkerboard-style matrix with unique values
board = np.arange(25).reshape(5, 5)
# [[ 0 1 2 3 4]
# [ 5 6 7 8 9]
# [10 11 12 13 14]
# [15 16 17 18 19]
# [20 21 22 23 24]]
# --- Four quadrants (center row/col = index 2) ---
top_left = board[:2, :2] # [[0 1], [5 6]]
top_right = board[:2, 3:] # [[3 4], [8 9]]
bottom_left = board[3:, :2] # [[15 16], [20 21]]
bottom_right = board[3:, 3:] # [[18 19], [23 24]]
print("Top-Left:\n", top_left)
print("Top-Right:\n", top_right)
print("Bottom-Left:\n", bottom_left)
print("Bottom-Right:\n", bottom_right)
# --- Main diagonal ---
diag = np.diag(board)
print("Main diagonal:", diag) # [0 6 12 18 24]
# --- Anti-diagonal ---
anti_diag = np.diag(np.fliplr(board))
print("Anti-diagonal:", anti_diag) # [4 8 12 16 20]
# --- Center 3x3 ---
center = board[1:4, 1:4]
print("Center 3x3:\n", center)
# [[ 6 7 8]
# [11 12 13]
# [16 17 18]]
# --- Verify view behavior ---
view_slice = board[1:4, 1:4]
print("Is view?", np.shares_memory(board, view_slice)) # True
view_slice[0, 0] = 999
print("Original after modifying view:")
print(board)
# Row 1, col 1 is now 999
# --- Safe copy ---
safe_copy = board[1:4, 1:4].copy()
safe_copy[:, :] = 0
print("Original after modifying copy (unchanged):")
print(board) # Only the 999 change persists; copy modification has no effect
> **输出:** 在本地 Python 环境运行 NumPy 2.x,输出 ndarray 数组内容。Piston 服务器未预装 NumPy,请在本机安装(`pip install numpy`)后实操对照。实际数值可能因 NumPy 版本、随机种子略有差异。
❓ 常见问题
.copy() 可以创建独立副本,修改副本不会影响原数组。[::-1] 怎么理解?a[::-1] 等价于从最后一个元素开始,步长 -1 向前遍历,结果就是反转。a[12:3:-2] 表示从索引 12 开始,每次向左跳 2 步,到索引 3(不含)停止。注意负步长时 start > stop。... 什么时候用?... 代替前面的所有 :。例如 4D 数组 a[:, :, :, 0] 可简写为 a[..., 0]。省略号只能出现一次,且自动填充到中间所有缺失的维度。.copy() 方法:safe_slice = a[2:5].copy()。这样得到的副本与原数组完全独立,修改副本不会影响原数组。也可以用 np.array(a[2:5]) 构造新数组,效果相同。a[[0, 2, 4]])返回视图还是副本?📖 小节
- 基本索引
a[i]取单个元素,切片a[start:stop:step]取子数组 - 多维索引用逗号分隔各维度:
a[row, col](2D)、a[d, row, col](3D) - 负索引从末尾倒数,
-1为最后一个元素 - 省略号
...可代替连续的:,简化高维索引 - 切片返回视图,与原数组共享内存;修改视图 = 修改原数组
- 使用
.copy()创建独立副本,避免意外修改原数组 - 切片赋值
a[2:5] = 99直接修改原数组对应位置 - 花式索引和布尔索引返回副本,不影响原数组
📝 作业
-
基础题(难度⭐):创建一个 5×5 的矩阵(元素 0~24),提取中心 3×3 子矩阵。打印结果并验证其 shape。
-
基础题(难度⭐):给定
a = np.arange(20),用步长切片分别取出:① 所有偶数索引位置的元素 ② 所有奇数索引位置的元素 ③ 反转后的数组。 -
进阶题(难度⭐⭐):创建一个 6×6 的数组,先用切片取第 2~4 行(索引 2、3、4),修改切片的值为 0,验证原数组也被修改。然后用
.copy()重新取相同的切片,修改副本为 -1,验证原数组不变。用np.shares_memory()辅助验证。