1.2 索引、切片与花式索引
第 1 章学会了创建数组,但数组里的数据如何按需取出和修改,还没有系统涉及。Python 列表的索引和切片规则在这里部分延续,但数组有更强大的能力:可以用整数数组一次取出多个位置的元素,可以用布尔数组做条件筛选,还可以沿着多个维度同时索引。掌握这些索引方式,是高效操作数组数据的核心技能。本节将依次讲解基本索引、切片、整数数组索引、布尔索引、混合索引、np.ix_() 构造网格,以及视图与副本的概念。对于学过R的同学大家会发现两者几乎一模一样,因为实际上 numpy 是启发自 R 语言的:)
1.2.1 基本索引 [i, j]
一维数组的索引
一维数组用整数索引单个元素,与列表相同。索引从 0 开始,支持负数索引从末尾倒数:
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 上的一个子数组):
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],但前者更高效,后者会先创建中间数组。
三维数组
三维数组有行、列、层三个轴,索引用三个数:
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]) # 41.2.2 切片索引(start:stop:step)及多维切片
一维切片
切片语法 start:stop:step,与列表切片一致,stop 不包含:
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,中间用冒号分隔:
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 列,得到一个一维数组。
多维切片的省略写法
高维数组切片时,后面的轴可以省略,省略的轴默认取全部:
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 整数数组索引(花式索引)
一维花式索引
用整数数组作为索引,可以一次取出多个任意位置的元素:
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]花式索引返回的是副本,与原数组不共享内存,这与切片返回视图的行为不同。
二维花式索引
对二维数组,整数数组索引沿每个轴取元素:
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 对应位置被取出:
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]条件生成掩码
掩码通常由比较运算生成,这是布尔索引的主要用途——按条件筛选:
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],偶数多个条件用 &(与)、|(或)、~(非)组合,注意每个条件都要加括号:
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]二维布尔掩码
对二维数组,布尔掩码可以是整个数组形状,或只沿一个轴:
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 混合索引
切片与整数/花式索引混用
一个轴的切片与另一个轴的整数或数组索引可以混合使用:
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]]切片与布尔索引混用
切片与布尔掩码也可以组合:
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] 的区别是:后者按位置配对,前者取行与列的所有组合:
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)
基本概念
视图是与原数组共享底层数据的数组,修改视图会影响原数组。副本是独立的内存,修改互不影响。理解这一点,才能避免修改数据时出现意外的连锁反应。
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() 创建独立副本:
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,不复制底层字节:
b = np.array([1, 2, 3], dtype=np.int32)
b_view = b.view(np.float32)
print(b_view) # 把 int32 数据按 float32 解释,结果无意义但共享内存这种方式一般用于底层二进制操作,日常使用较少。
1.2.8 索引赋值操作
切片赋值
切片作为赋值目标时,把右侧数据写入切出的位置:
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]布尔索引赋值
布尔掩码赋值只改满足条件的元素,这是数据清洗中批量替换值的主要手段:
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]花式索引赋值
整数数组索引也可以作为赋值目标,但注意不能有重复索引,否则结果不确定:
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题 概念理解
写出下面代码的输出结果,并说明切片与花式索引在是否返回视图上的区别。
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])])参考答案
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。
参考答案
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,并说明该操作是否影响原数组。
参考答案
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 的任务分别有多少个。
参考答案
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) 检查。