6.6. 索引与切片

ndarray 有四种寻址方式:单个索引、切片、布尔掩码,以及它们各自的赋值形式。

6.6.1. 单个元素

方括号索引返回给定位置上的值:

a = np.arange(10, dtype=np.uint8)
print(a[0], a[-1])      # 0 9
print(a[1], a[-2])      # 1 8

负索引从末尾开始计数,与 Python list 相同。超出范围的索引会引发 IndexError

对于高维数组,每个轴都需要一个索引。这些索引放在同一组方括号内,用逗号分隔:

m = np.arange(9, dtype=np.uint8).reshape((3, 3))
print(m[1, 1])          # 4
print(m[2, 0])          # 6

当提供的索引少于轴数时,未被索引的轴保持原样。结果是源的一个降维视图:

print(m[0])             # the first row, as a 1-D view of m

6.6.2. 切片

切片 start:stop:step 返回数组的一个视图。该视图与源共享底层数据缓冲区;通过视图写入会写入到源:

a = np.arange(10, dtype=np.uint8)
v = a[::2]              # array([0, 2, 4, 6, 8], dtype=uint8)
v[0] = 99
print(a)
# array([99, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=uint8)

当需要一个独立缓冲区时,copy() 可以显式生成一个。

切片自然地扩展到更高维度。每个轴都有自己的切片:

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

m[0]            # first row
m[0, :2]        # first two elements of row 0
m[:, 0]         # column 0 (still 2-D in ulab)
m[-1]           # last row
m[::2, ::2]     # every other row, every other column

可以混用整数(单个索引,会丢弃该轴)和切片(保留该轴),这正是通常用来访问单行/单列的写法。

6.6.3. 布尔掩码

一个与源形状相同的布尔数组会选出掩码为 True 的那些元素。布尔索引目前适用于一维数组;更高维的输入会引发 NotImplementedError:

a = np.arange(9, dtype=np.float)
mask = a < 5
print(a[mask])

输出:

array([0.0, 1.0, 2.0, 3.0, 4.0], dtype=float)

掩码是一个普通的 bool ndarray,因此任何能产生布尔数组的表达式都可以使用:

b = np.array([4, 4, 4, 3, 3, 3, 13, 13, 13], dtype=np.uint8)
a = np.arange(9, dtype=np.uint8)
print(a[a * a > np.sin(b) * 100.0])

布尔索引返回一份副本。被选中的元素位于掩码为 True 的任意位置——而不是沿源以固定步长排列——所以没有任何描述符能让视图来寻址它们,结果会被具化到它自己的缓冲区中。

6.6.4. 整数数组索引

在方括号中传入一个索引列表或数组,可以一步选出这些元素:

a = np.array([10, 20, 30, 40, 50], dtype=np.uint8)
a[[0, 2, 4]]
# array([10, 30, 50], dtype=uint8)

结果是一份副本;被选出的元素不再与源共享存储。同样的形式也可以用在赋值号的左侧:

a[[0, 2, 4]] = 0
# array([0, 20, 0, 40, 0], dtype=uint8)

take()(在 选择与重排 中介绍)是这一相同操作的函数形式,它接受 out= 关键字,可在流式循环中实现免分配使用。

6.6.5. 切片赋值

切片和掩码既可以出现在赋值号的侧,也可以出现在左侧。右侧可以是标量、另一个数组或一个视图:

m = np.zeros((3, 3), dtype=np.uint8)
m[0]      = 1            # whole row 0 set to 1
m[:, 2]   = 3            # whole column 2 set to 3
m[1, 1:3] = [7, 8]       # row 1, columns 1 and 2

出现在左侧的布尔掩码会替换满足条件的那些元素:

a = np.arange(9, dtype=np.uint8)
a[a < 3] = 99
# array([99, 99, 99, 3, 4, 5, 6, 7, 8], dtype=uint8)

a = np.arange(9, dtype=np.uint8)
b = np.array(range(9)) + 12
a[b < 15] = b[b < 15]
# array([12, 13, 14, 3, 4, 5, 6, 7, 8], dtype=uint8)

6.6.6. 为什么切片赋值在摄像头上很重要

切片赋值通过一个已经存在的数组进行写入。不会分配新数组。这正是以下两者之间的区别:

out = a + b              # makes a new array the size of a
out = out * 2            # makes another new array

以及:

out[:] = a               # writes into the existing out
out   += b               # in place
out   *= 2               # in place

第一种版本会向摄像头请求相当于两个新数组大小的 RAM;第二种版本则不请求任何内存。在 RAM 有限的微控制器上,这一差别往往就是脚本能否从容运行与是否耗尽内存之间的差别。

性能 详细介绍了这一模式。目前最重要的规则是:切片赋值、原地算术运算符(+=*= 等)以及通用函数上的 out= 关键字,是实现免分配更新的三大工具。