Skip to content

1.2 索引、切片与花式索引 ​

第 1 章学会了创建数组,但数组里的数据如何按需取出和修改,还没有系统涉及。Python 列表的索引和切片规则在这里部分延续,但数组有更强大的能力:可以用整数数组一次取出多个位置的元素,可以用布尔数组做条件筛选,还可以沿着多个维度同时索引。掌握这些索引方式,是高效操作数组数据的核心技能。本节将依次讲解基本索引、切片、整数数组索引、布尔索引、混合索引、np.ix_() 构造网格,以及视图与副本的概念。对于学过R的同学大家会发现两者几乎一模一样,因为实际上 numpy 是启发自 R 语言的:)

1.2.1 基本索引 [i, j] ​

一维数组的索引 ​

一维数组用整数索引单个元素,与列表相同。索引从 0 开始,支持负数索引从末尾倒数:

python
import numpy as np

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

多维数组的索引 ​

二维数组用逗号分隔多个轴的索引,a[i, j] 表示第 i 行第 j 列。单独传一个整数给二维数组,取到的是整行(轴 0 上的一个子数组):

python
import numpy as np

a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(a[0])        # [1 2 3],第 0 行
print(a[0, 1])     # 2,第 0 行第 1 列
print(a[1, 2])     # 6
print(a[-1, -1])   # 9,右下角元素

a[0, 1] 中的逗号是多个轴索引的分隔,等价于 a[0][1],但前者更高效,后者会先创建中间数组。

三维数组 ​

三维数组有行、列、层三个轴,索引用三个数:

python
import numpy as np

a = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
print(a.shape)     # (2, 2, 2)
print(a[0])        # [[1 2] [3 4]],第一个 2x2 平面
print(a[0, 1])     # [3 4]
print(a[0, 1, 1])  # 4

1.2.2 切片索引(start:stop:step)及多维切片 ​

一维切片 ​

切片语法 start:stop:step,与列表切片一致,stop 不包含:

python
import numpy as np

a = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
print(a[2:6])       # [2 3 4 5]
print(a[:4])        # [0 1 2 3]
print(a[4:])        # [4 5 6 7 8 9]
print(a[::2])       # [0 2 4 6 8],隔一个取一个
print(a[::-1])      # [9 8 7 6 5 4 3 2 1 0],反转
print(a[2:8:3])     # [2 5]

负步长实现反转。切片始终返回新的视图(共享内存),不会复制数据。

多维切片 ​

多维切片沿每个轴分别写 start:stop:step,中间用冒号分隔:

python
import numpy as np

a = np.arange(16).reshape(4, 4)
print(a)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]
#  [12 13 14 15]]

print(a[1:3, 1:3])     # 取中间 2x2 子块
# [[ 5  6]
#  [ 9 10]]

print(a[::2, ::2])     # 隔行隔列取
# [[ 0  2]
#  [ 8 10]]

print(a[:, 1])         # 所有行的第 1 列
# [1 5 9 13]

print(a[1, :])         # 第 1 行的所有列
# [4 5 6 7]

: 单独写表示该轴全部。a[:, 1] 取所有行的第 1 列,得到一个一维数组。

多维切片的省略写法 ​

高维数组切片时,后面的轴可以省略,省略的轴默认取全部:

python
a = np.arange(24).reshape(2, 3, 4)
print(a.shape)            # (2, 3, 4)
print(a[0].shape)         # (3, 4)
print(a[0, 1].shape)      # (4,)
print(a[0, 1, :].shape)   # (4,),等价于上一行

1.2.3 整数数组索引(花式索引) ​

一维花式索引 ​

用整数数组作为索引,可以一次取出多个任意位置的元素:

python
import numpy as np

a = np.array([10, 20, 30, 40, 50, 60])
idx = np.array([0, 2, 4])
print(a[idx])       # [10 30 50]

# 结果顺序与索引顺序一致,允许重复
print(a[[5, 1, 1]])  # [60 20 20]

花式索引返回的是副本,与原数组不共享内存,这与切片返回视图的行为不同。

二维花式索引 ​

对二维数组,整数数组索引沿每个轴取元素:

python
import numpy as np

a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
row = np.array([0, 2])
print(a[row])        # [[1 2 3] [7 8 9]],取第 0 行和第 2 行

# 用两个索引数组分别指定行和列,成对取元素
col = np.array([0, 1])
print(a[row, col])   # [1 8],(0,0) 和 (2,1)

a[row, col] 中 row 和 col 长度相同,对应位置组成坐标对。要取子块(行和列的所有组合),需要用 np.ix_()。

1.2.4 布尔索引(布尔数组作为掩码) ​

一维布尔掩码 ​

用一个与数组形状相同的布尔数组作为索引,True 对应位置被取出:

python
import numpy as np

a = np.array([1, 2, 3, 4, 5, 6])
mask = np.array([True, False, True, False, True, False])
print(a[mask])    # [1 3 5]

条件生成掩码 ​

掩码通常由比较运算生成,这是布尔索引的主要用途——按条件筛选:

python
a = np.array([1, 2, 3, 4, 5, 6])
print(a > 3)         # [False False False  True  True  True]
print(a[a > 3])      # [4 5 6],选出大于 3 的元素
print(a[a % 2 == 0]) # [2 4 6],偶数

多个条件用 &(与)、|(或)、~(非)组合,注意每个条件都要加括号:

python
a = np.array([1, 2, 3, 4, 5, 6])
print(a[(a > 2) & (a < 5)])    # [3 4]
print(a[(a < 2) | (a > 5)])    # [1 6]
print(a[~(a > 3)])             # [1 2 3]

二维布尔掩码 ​

对二维数组,布尔掩码可以是整个数组形状,或只沿一个轴:

python
import numpy as np

a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
print(a[a > 5])      # [6 7 8 9],展平后的一维结果

# 一维掩码只作用于第一个轴,按行筛选
rows = np.array([True, False, True])
print(a[rows])
# [[1 2 3]
#  [7 8 9]]

a[a > 5] 的结果是一维数组,因为布尔掩码选定的是散落的元素。

1.2.5 混合索引 ​

切片与整数/花式索引混用 ​

一个轴的切片与另一个轴的整数或数组索引可以混合使用:

python
import numpy as np

a = np.arange(16).reshape(4, 4)
print(a)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]
#  [12 13 14 15]]

print(a[1:3, 0])      # 第 1~2 行的第 0 列 -> [4 8]
print(a[1, :3])       # 第 1 行的前 3 列 -> [4 5 6]
print(a[::2, [0, 2]]) # 隔行的第 0、2 列
# [[ 0  2]
#  [ 8 10]]

切片与布尔索引混用 ​

切片与布尔掩码也可以组合:

python
mask = np.array([True, False, True, False])
print(a[mask, :2])   # 掩码行筛选后,再取前两列
# [[0 1]
#  [8 9]]

1.2.6 np.ix_() 函数构造索引网格 ​

np.ix_() 把两个一维索引数组组合成用于取子块的花式索引网格。它与 a[row, col] 的区别是:后者按位置配对,前者取行与列的所有组合:

python
import numpy as np

a = np.arange(12).reshape(3, 4)
print(a)
# [[ 0  1  2  3]
#  [ 4  5  6  7]
#  [ 8  9 10 11]]

row = np.array([0, 2])
col = np.array([1, 3])

# 不用 ix_,按位置配对,只取两个点
print(a[row, col])   # [1 11],(0,1) 和 (2,3)

# 用 ix_,取所有组合的 2x2 子块
print(a[np.ix_(row, col)])
# [[ 1  3]
#  [ 9 11]]

np.ix_() 常用于从矩阵中抽取指定行和指定列交叉的子矩阵。

1.2.7 视图(view)与副本(copy) ​

基本概念 ​

视图是与原数组共享底层数据的数组,修改视图会影响原数组。副本是独立的内存,修改互不影响。理解这一点,才能避免修改数据时出现意外的连锁反应。

python
import numpy as np

a = np.arange(6).reshape(2, 3)
print(a)
# [[0 1 2]
#  [3 4 5]]

# 切片返回视图
v = a[0:2, 0:2]
v[0, 0] = 99
print(a)   # 原数组也被修改
# [[99  1  2]
#  [ 3  4  5]]

# 副本独立
c = a.copy()
c[0, 0] = -1
print(a[0, 0])   # 99,不受影响
print(c[0, 0])   # -1

哪些操作返回视图,哪些返回副本 ​

常见规则如下:

操作结果
切片 a[1:3]视图
基本索引 a[0]视图(子数组)
花式索引 a[[0, 2]]副本
布尔索引 a[a > 3]副本
a.copy() / np.copy(a)副本
.reshape()、.transpose()视图(能表示就共享)
.astype()副本(类型不同必复制)

用 np.shares_memory(a, b) 判断是否共享数据,用 a.base 查看数组是否基于其他数组。

.view() 与 .copy() 方法 ​

.view() 显式创建一个共享数据的新数组,.copy() 创建独立副本:

python
import numpy as np

a = np.array([[1, 2, 3], [4, 5, 6]])
v = a.view()      # 共享数据
c = a.copy()      # 独立数据

print(np.shares_memory(a, v))   # True
print(np.shares_memory(a, c))   # False

.view() 还能用于重新解释数据的 dtype,不复制底层字节:

python
b = np.array([1, 2, 3], dtype=np.int32)
b_view = b.view(np.float32)
print(b_view)     # 把 int32 数据按 float32 解释,结果无意义但共享内存

这种方式一般用于底层二进制操作,日常使用较少。

1.2.8 索引赋值操作 ​

切片赋值 ​

切片作为赋值目标时,把右侧数据写入切出的位置:

python
import numpy as np

a = np.arange(10)
a[2:5] = 100          # 把位置 2~4 全部设为 100
print(a)              # [0 1 100 100 100 5 6 7 8 9]

a[2:5] = [10, 20, 30] # 逐元素赋值,长度需匹配
print(a)              # [0 1 10 20 30 5 6 7 8 9]

布尔索引赋值 ​

布尔掩码赋值只改满足条件的元素,这是数据清洗中批量替换值的主要手段:

python
import numpy as np

a = np.array([1, 2, 3, 4, 5, 6])
a[a > 3] = -1      # 把大于 3 的元素全部替换为 -1
print(a)           # [ 1  2  3 -1 -1 -1]

花式索引赋值 ​

整数数组索引也可以作为赋值目标,但注意不能有重复索引,否则结果不确定:

python
a = np.zeros(5)
a[[0, 2, 4]] = 1    # 位置 0、2、4 设为 1
print(a)            # [1. 0. 1. 0. 1.]

# 重复索引的赋值结果取决于执行顺序,应避免
b = np.zeros(5)
b[[0, 0]] = [1, 2]  # 结果可能是 2 或 1,不确定
print(b)

练习题 ​

第1题 概念理解 ​

写出下面代码的输出结果,并说明切片与花式索引在是否返回视图上的区别。

python
import numpy as np

a = np.arange(12).reshape(3, 4)
print(a[1, 2])
print(a[:, 1])
print(a[::2, 1:3])
print(a[[0, 2], [1, 3]])
print(a[np.ix_([0, 2], [1, 3])])
参考答案
python
6
[1 5 9]
[[1 2]
 [9 10]]
[1 11]
[[1 3]
 [9 11]]

切片 a[:, 1]、a[::2, 1:3] 返回视图;花式索引 a[[0, 2], [1, 3]]、np.ix_ 返回副本。

第2题 代码编写 ​

给定数组 data = np.array([12, 5, 9, 20, 7, 15, 3, 8]),用布尔索引筛选出所有大于 10 的元素;再用掩码把所有偶数替换为 0。

参考答案
python
import numpy as np

data = np.array([12, 5, 9, 20, 7, 15, 3, 8])
print(data[data > 10])   # [12 20 15]

data[data % 2 == 0] = 0
print(data)              # [ 0  5  9  0  7 15  3  0]

第3题 进阶练习 ​

对 a = np.arange(25).reshape(5, 5),用切片取出:第 1 行到第 3 行、第 1 列到第 3 列交叉的 3x3 子块;再用布尔掩码取出所有大于 20 的元素;最后把中心元素(第 2 行第 2 列)改为 -1,并说明该操作是否影响原数组。

参考答案
python
import numpy as np

a = np.arange(25).reshape(5, 5)
sub = a[1:4, 1:4]         # 3x3 子块
print(sub)

print(a[a > 20])          # [21 22 23 24]

a[2, 2] = -1              # 基本索引,修改原数组
print(a[2, 2])            # -1

切片 a[1:4, 1:4] 返回视图,a[2, 2] = -1 是基本索引赋值,直接写入原数组。

第4题 项目实践 ​

命令行任务管理器要统计各任务的紧急程度。假设紧急程度存储在数组 urgency = np.array([3, 1, 5, 2, 4, 1, 5]) 中。用布尔索引筛选出所有紧急程度为 5 的任务位置(用 np.where 或布尔索引),并统计紧急程度为 1 和 5 的任务分别有多少个。

参考答案
python
import numpy as np

urgency = np.array([3, 1, 5, 2, 4, 1, 5])
print(urgency[urgency == 5])           # [5 5],紧急程度为 5 的值
print(np.where(urgency == 5))          # (array([2, 6]),),位置
print(np.count_nonzero(urgency == 1))  # 2
print(np.count_nonzero(urgency == 5))  # 2

常见错误 ​

错误 1 · IndexError: index 3 is out of bounds for axis 0 with size 3

原因:索引值超出数组范围。例如对 3 个元素的数组访问 a[3],索引最大只能到 2。

解决:检查数组 shape,确认索引在 0 ~ size-1 范围内。不确定大小时可用 len(a) 或 a.shape 查看。

错误 2 · 布尔索引时忘了加括号

原因:写 a[a > 2 & a < 5],运算符优先级导致 & 先与 2 结合,报 TypeError: ufunc 'bitwise_and' not supported 或结果错误。

解决:每个比较条件单独加括号:a[(a > 2) & (a < 5)]。

错误 3 · 用 and/or 代替 &/|

原因:Python 的 and/or 返回布尔值,不能用于 NumPy 数组的逐元素逻辑运算,会报 ValueError: The truth value of an array... is ambiguous。

解决:数组逻辑运算用 &、|、~。

错误 4 · 修改切片数据导致原数组被意外修改

原因:切片返回视图,共享底层数据。修改切片的内容会同步改变原数组。

解决:需要独立数据时用 .copy();不确定是否共享时用 np.shares_memory(a, b) 检查。