Skip to content

第 3 章 索引与切片

学习目标

  • 掌握一维数组的正向与负向索引
  • 掌握一维切片 start:stop:step
  • 掌握二维数组的行、列、子块索引
  • 理解「切片是视图」,能区分视图与副本

3.1 一维索引

索引是用下标取出数组中的单个元素。下标从 0 开始;负下标从末尾倒数,-1 是最后一个元素:

python
import numpy as np

a = np.array([10, 20, 30, 40, 50])
print(a[0])     # 第一个
print(a[2])     # 第三个
print(a[-1])    # 最后一个
print(a[-2])    # 倒数第二个

输出:

10
30
50
40

越界访问会报 IndexError:

>>> a[5]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: index 5 is out of bounds for axis 0 with size 5

解读:axis 0(第 0 轴)上数组大小是 5,下标 5 越界。合法下标是 0~4(或 -1~-5)。

3.2 一维切片

切片数组[起始:结束:步长] 取出一段连续(或按步长跳跃)的元素。规则与 Python 列表一致:

  • 结束 不包含
  • 起始 省略表示从头,结束 省略表示到尾
  • 步长 省略表示 1
python
import numpy as np

a = np.array([10, 20, 30, 40, 50])
print(a[1:4])    # 下标 1 到 3
print(a[:3])     # 前 3 个
print(a[2:])     # 从下标 2 到最后
print(a[::2])    # 隔一个取一个
print(a[::-1])   # 反转

输出:

[20 30 40]
[10 20 30]
[30 40 50]
[10 30 50]
[50 40 30 20 10]

步长为负数时方向反转:[::-1] 是倒序,[7:2:-2] 从下标 7 往回取到下标 3(不含 2),步长 -2:

python
import numpy as np

print(np.arange(10)[7:2:-2])   # 7, 5, 3

输出:

[7 5 3]

3.3 二维索引

二维数组 a[r, c]:第一个下标是,第二个是。省略第二个下标取整行。

python
import numpy as np

b = np.array([[1, 2, 3],
              [4, 5, 6],
              [7, 8, 9]])

print(b[0])        # 第 0 行
print(b[0, 1])     # 第 0 行第 1 列 → 2
print(b[1, 1])     # 第 1 行第 1 列 → 5
print(b[:, 1])     # 所有行的第 1 列
print(b[1:, :2])   # 第 1 行起,前 2 列

输出:

[1 2 3]
2
5
[2 5 8]
[[4 5]
 [7 8]]

重点理解 b[:, 1]:冒号在行的位置表示「所有行」,1 在列的位置表示第 1 列,合起来就是「所有行的第 1 列」——取出整列。这是二维数组最常用的索引写法之一。

3.4 切片是视图,不是副本

这是 NumPy 与 Python 列表最重要的区别之一。

视图(view)是原数组的一块「窗口」:切片得到的新数组共享底层数据。修改视图,原数组跟着变。

python
import numpy as np

a = np.arange(10)        # [0 1 2 3 4 5 6 7 8 9]
sub = a[2:5]             # 视图:指向 a 的 2~4 号元素
print(sub)

sub[0] = 99              # 改视图
print(a)                 # 原数组也被改了!

输出:

[2 3 4]
[ 0  1 99  3  4  5  6  7  8  9]

sub[0]a[2] 是同一块内存。视图机制让切片几乎零成本(不用复制数据),但初学者常被「改切片影响了原数组」吓到。规则只有一条:凡是用 切片整数下标+切片 取出来的都是视图;显式调用 .copy() 才得到独立副本。

二维同理:

python
import numpy as np

m = np.arange(12).reshape(3, 4)
row = m[1]               # 第 1 行,是视图
row[0] = 99
print(m)

输出:

[[ 0  1  2  3]
 [99  5  6  7]
 [ 8  9 10 11]]

需要独立副本时:

python
import numpy as np

a = np.arange(10)
c = a[2:5].copy()        # 独立副本
c[0] = -1
print(a)                 # 原数组不变
print(c)

输出:

[0 1 2 3 4 5 6 7 8 9]
[-1 3 4]

3.5 用切片赋值

既然切片是视图,给它赋值就是「批量修改原数组的对应位置」:

python
import numpy as np

m = np.arange(12).reshape(3, 4)
m[:, 1] = 0              # 第 1 列全部置 0
print(m)

输出:

[[ 0  0  2  3]
 [ 4  0  6  7]
 [ 8  0 10 11]]

这是数据清洗里最常用的操作之一:批量把某一列(或某一段)替换成固定值。

3.6 花式索引与布尔索引(预告)

除了下标和切片,NumPy 还支持两类更强大的索引,先混个脸熟,细节分别在后续章节展开:

  • 花式索引:用整数数组当下标,取不连续的元素,得到的是副本(不是视图):
python
import numpy as np

a = np.arange(10)
idx = np.array([0, 2, 4])
print(a[idx])

输出:

[0 2 4]
  • 布尔索引:用布尔数组过滤,留下 True 对应的元素(第 8 章正式讲):
python
import numpy as np

a = np.array([10, 20, 30, 40, 50])
print(a[a > 30])

输出:

[40 50]

动手实践

  1. 创建 np.arange(20),用切片取出:前 5 个、后 5 个、偶数下标元素、倒序。
  2. 创建 4×4 数组 np.arange(16).reshape(4, 4),分别取出:第 2 行、第 3 列、2~3 行、右上角 2×2 子块。
  3. 验证视图:取一行切片并修改它,打印原数组,确认被改动;再用 .copy() 重复一次,确认不被改动。
  4. 用切片赋值把 4×4 数组的对角线全部置为 0(提示:对角线元素是 [0,0][1,1][2,2][3,3],可以用 np.arange(4) 做行下标与列下标)。

常见错误

错误写法现象原因
a[5](数组长度 5)IndexError: index 5 is out of bounds合法下标 0~4;越界了
a[1:4] 期望包含下标 4结果不含 a[4]切片 结束 不包含
修改切片后惊讶「原数组变了」原数组被连带修改切片是视图,共享数据;需要独立用 .copy()
b[1][1]b[1, 1] 混淆两者结果相同二维建议统一写 b[1, 1],语义更清楚
b[:] 复制数组修改它影响了原数组b[:] 是视图;复制用 b.copy()
m[1, ] 多写一个逗号行为异常或报错单个下标取行即可,写成 m[1]

章末练习

基础

  1. a = np.array([5, 10, 15, 20, 25]),用索引分别取出 5、25、15 三个元素。
  2. 用切片取出 a[1:4]a[:3]a[::2],并写出各自的结果。
  3. 创建 np.arange(9).reshape(3, 3),取出第 0 行、第 2 列、[1:, :2] 子块。

提高

  1. 创建 5×5 数组 np.arange(25).reshape(5, 5),用切片赋值把最外层边框(第 0 行、第 4 行、第 0 列、第 4 列)全部改为 -1,打印结果。
  2. 解释:为什么 sub = a[2:5]; sub[0] = 99 会改变 a?如何避免?

挑战

  1. 不用循环,写出一段代码把 4×4 数组的主对角线元素全部加 10(提示:i = np.arange(4),用 m[i, i] += 10 试试)。
  2. np.arange(24).reshape(4, 6) 取出「第 1 行、第 2~4 行、隔列取」等组合切片,验证视图与副本的区别,并总结:哪些索引方式返回视图,哪些返回副本。

章末自测

每题选择一个最佳答案。本书不附答案:完成后交由老师或 AI 老师批改讲解。

  1. a = np.array([10, 20, 30]); a[-1] 的结果是?
    • A. 10
    • B. 30
    • C. 20
    • D. 报错
  2. np.arange(10)[2:5] 的结果是?
    • A. [2, 3, 4]
    • B. [2, 3, 4, 5]
    • C. [0, 1, 2]
    • D. [5, 6, 7]
  3. np.arange(10)[::2] 的结果是?
    • A. [0, 2, 4, 6, 8]
    • B. [1, 3, 5, 7, 9]
    • C. [0, 1, 2, 3, 4]
    • D. [9, 7, 5, 3, 1]
  4. b = np.array([[1, 2], [3, 4]]); b[0, 1] 的结果是?
    • A. 1
    • B. 2
    • C. 3
    • D. [1, 2]
  5. 取出二维数组所有行的第 0 列,写法是?
    • A. b[0]
    • B. b[:, 0]
    • C. b[0, :]
    • D. b[0, 0]
  6. 关于切片视图,正确的说法是?
    • A. 切片总是返回独立副本
    • B. 切片返回视图,修改它会改变原数组
    • C. 切片不能用于二维数组
    • D. 视图与副本没有区别
  7. 获得数组的独立副本,正确写法是?
    • A. a[:]
    • B. a.copy()
    • C. a.view()
    • D. a.clone()
  8. np.arange(10)[7:2:-2] 的结果是?
    • A. [7, 5, 3]
    • B. [7, 5, 3, 2]
    • C. [5, 3]
    • D. []
  9. 越界索引 a[5](长度 5)会抛出?
    • A. ValueError
    • B. IndexError
    • C. TypeError
    • D. KeyError
  10. m[:, 1] = 0 的作用是?
    • A. 把第 1 行全部置 0
    • B. 把第 1 列全部置 0
    • C. 报错
    • D. 把对角线置 0