
2.1.5 多维数组
多维数组的存取和一维数组类似,因为多维数组有多个轴,所以它的下标需要用多个值来表示。NumPy采用元组作为数组的下标,元组中的每个元素和数组的每个轴对应。图2-1显示了一个shape为(6, 6)的数组a,图中用不同颜色和线型标出各个下标所对应的选择区域。

图2-1 使用数组切片语法访问多维数组中的元素
为什么使用元组作为下标
Python的下标语法(用[]存取序列中的元素)本身并不支持多维,但是可以使用任何对象作为下标,因此NumPy使用元组作为下标存取数组中的元素,使用元组可以很方便地表示多个轴的下标。虽然在Python程序中经常用圆括号将元组的元素括起来,但其实元组的语法只需要用逗号隔开元素即可,例如x, y=y, x就是用元组交换变量值的一个例子。因此a[1, 2]和a[(1, 2)]完全相同,都是使用元组(1,2)作为数组a的下标。
读者也许会对如何创建图中的二维数组感到好奇。它实际上是一个加法表,由纵向量(0, 10, 20, 30, 40, 50)和横向量(0, 1, 2, 3, 4, 5)的元素相加而得。可以用下面的语句创建它,至于其原理,将在后面的章节进行讨论。
a = np.arange(0, 60, 10).reshape(-1, 1) + np.arange(0, 6) a array([[ 0, 1, 2, 3, 4, 5], [10, 11, 12, 13, 14, 15], [20, 21, 22, 23, 24, 25], [30, 31, 32, 33, 34, 35], [40, 41, 42, 43, 44, 45], [50, 51, 52, 53, 54, 55]])
图2-1中的下标都是有两个元素的元组,其中的第0个元素与数组的第0轴(纵轴)对应,而第1个元素与数组的第1轴(横轴)对应。下面是图中各种多维数组切片的运算结果:

如果下标元组中只包含整数和切片,那么得到的数组和原始数组共享数据,它是原数组的视图。下面的例子中,数组b是a的视图,它们共享数据,因此修改b[0]时,数组a中对应的元素也被修改:
b = a[0, 3:5] b[0] = -b[0] a[0, 3:5] array([-3, 4])
因为数组的下标是一个元组,所以我们可以将下标元组保存起来,用同一个元组存取多个数组。在下面的例子中,a[idx]和a[::2,2:]相同,a[idx][idx]和a[::2,2:][::2,2:]相同。
idx = slice(None, None, 2), slice(2,None) a[idx] a[idx][idx] ----------------------------- [[ 2, -3, 4, 5], [[ 4, 5], [22, 23, 24, 25], [44, 45]] [42, 43, 44, 45]]
切片(slice)对象
根据Python的语法,在[]中可以使用以冒号隔开的两个或三个整数表示切片,但是单独生成切片对象时需要使用slice()来创建。它有三个参数,分别为开始值、结束值和间隔步长,当这些值需要省略时可以使用None。例如,a[slice(None,None,None),2]和a[:, 2]相同。
用Python的内置函数slice()创建下标比较麻烦,因此NumPy提供了一个s_对象来帮助我们创建数组下标,请注意s_实际上是IndexExpression类的一个对象:
np.s_[::2, 2:] (slice(None, None, 2), slice(2, None, None))
s_为什么不是函数
根据Python的语法,只有在中括号[]中才能使用以冒号隔开的切片语法,如果s_是函数,那么这些切片必须使用slice()创建。类似的对象还有mgrid和ogrid等,后面我们会学习它们的用法。Python的下标语法实际上会调用__getitem__()方法,因此我们可以很容易自己实现s_对象的功能:
class S(object): def __getitem__(self, index): return index
在多维数组的下标元组中,也可以使用整数元组或列表、整数数组和布尔数组,如图2-2所示。当下标中使用这些对象时,所获得的数据是原始数据的副本,因此修改结果数组不会改变原始数组。

图2-2 使用整数序列和布尔数组访问多维数组中的元素
在a[(0,1,2,3),(1,2,3,4)]中,下标仍然是一个有两个元素的元组,元组中的每个元素都是一个整数元组,分别对应数组的第0轴和第1轴。从两个序列的对应位置取出两个整数组成下标,于是得到的结果是:a[0,1]、a[1,2]、a[2,3]、a[3,4]。
a[(0,1,2,3),(1,2,3,4)] array([ 1, 12, 23, 34])
在a[3:, [0,2,5]]中,第0轴的下标是一个切片对象,它选取第3行之后的所有行;第1轴的下标是整数列表,它选取第0、第2和第5列。
a[3:, [0,2,5]] array([[30, 32, 35], [40, 42, 45], [50, 52, 55]])
在a[mask, 2]中,第0轴的下标是一个布尔数组,它选取第0、第2和第5行;第1轴的下标是一个整数,它选取第2列。
mask = np.array([1,0,1,0,0,1], dtype=np.bool) a[mask, 2] array([ 2, 22, 52])
注意,如果mask不是布尔数组而是整数数组、列表或元组,就按照以整数数组作为下标的方式进行运算:
mask1 = np.array([1,0,1,0,0,1]) mask2 = [True,False,True,False,False,True] a[mask1, 2] a[mask2, 2] ------------------------ ------------------------ [12, 2, 12, 2, 2, 12] [12, 2, 12, 2, 2, 12]
当下标的长度小于数组的维数时,剩余的各轴所对应的下标是“:”,即选取它们的所有数据:
a[[1,2],:] a[[1,2]] -------------------------- -------------------------- [[10, 11, 12, 13, 14, 15], [[10, 11, 12, 13, 14, 15], [20, 21, 22, 23, 24, 25]] [20, 21, 22, 23, 24, 25]]
当所有轴都用形状相同的整数数组作为下标时,得到的数组和下标数组的形状相同:
x = np.array([[0,1],[2,3]]) y = np.array([[-1,-2],[-3,-4]]) a[x,y] array([[ 5, 14], [23, 32]])
效果和下面的程序相同:
a[(0,1,2,3),(-1,-2,-3,-4)].reshape(2,2) array([[ 5, 14], [23, 32]])
当没有指定第1轴的下标时,使用“:”作为下标,因此得到了一个三维数组:
a[x] array([[[ 0, 1, 2, -3, 4, 5], [10, 11, 12, 13, 14, 15]], [[20, 21, 22, 23, 24, 25], [30, 31, 32, 33, 34, 35]]])
可以使用这种以整数数组作为下标的方式快速替换数组中的每个元素,例如有一个表示索引图像的数组image,以及一个调色板数组palette,则palette[image]可以得到通过调色板着色之后的彩色图像:
palette = np.array( [ [0,0,0], [255,0,0], [0,255,0], [0,0,255], [255,255,255] ] ) image = np.array( [ [ 0, 1, 2, 0 ], [ 0, 3, 4, 0 ] ] ) palette[image] array([[[ 0, 0, 0], [255, 0, 0], [ 0, 255, 0], [ 0, 0, 0]], [[ 0, 0, 0], [ 0, 0, 255], [255, 255, 255], [ 0, 0, 0]]])