索引 NumPy 数组

日期2008-06-06(最后修改),2008-06-01(创建)

NumPy 的核心是引入一个多维数组对象,用于保存同类型数值数据。这当然是一个用于存储数据的有用工具,但它也能够在不编写低效的 Python 循环的情况下操作大量值。为了实现这一点,需要能够以多种不同的方式引用数组的元素,从简单的“切片”到使用数组作为查找表。本页面的目的是介绍各种可用的索引类型。希望有时奇怪的语法也能变得更加清晰。

我们将尽可能使用相同的数组作为示例

In [1]
import numpy as np
A = np.arange(10)
In [2]
A
Out[2]
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [3]
B = np.reshape(np.arange(9),(3,3))
B
Out[3]
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
In [4]
C = np.reshape(np.arange(2*3*4),(2,3,4))
C
Out[4]
array([[[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11]],

       [[12, 13, 14, 15],
        [16, 17, 18, 19],
        [20, 21, 22, 23]]])

元素

选择数组中一个或多个元素的最简单方法看起来非常类似于 Python 列表

In [5]
A[1]
Out[5]
1
In [6]
B[1,0]
Out[6]
3
In [7]
C[1,0,2]
Out[7]
14

也就是说,要选出一个特定的元素,只需在它后面加上方括号中的索引。正如 Python 的标准,元素编号从零开始。

如果您想就地更改数组值,只需在赋值中使用上面的语法即可。

In [8]
T = A.copy()
T[3] = -5
T
Out[8]
array([ 0,  1,  2, -5,  4,  5,  6,  7,  8,  9])
In [9]
T[0] += 7
T
Out[9]
array([ 7,  1,  2, -5,  4,  5,  6,  7,  8,  9])

(使用 .copy() 的目的是确保我们不会真正修改 A,因为这会使后面的示例变得混乱。) 请注意,numpy 也支持 Python 的“增量赋值”运算符,例如 +=、-=、*= 等。

请注意,数组元素的类型是数组本身的属性,因此,如果您尝试将另一个类型的元素分配给数组,它将被静默转换(如果可能)。

In [10]
T = A.copy()
T[3] = -1.5
T
Out[10]
array([ 0,  1,  2, -1,  4,  5,  6,  7,  8,  9])
In [12]
T[3] = -0.5j
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-12-513dc3e86c66> in <module>()
----> 1 T[3] = -0.5j

TypeError: can't convert complex to long
In [13]
T
Out[13]
array([ 0,  1,  2, -1,  4,  5,  6,  7,  8,  9])

请注意,发生的转换是默认转换;在 float 到 int 转换的情况下,它是截断。如果您想要其他操作,例如取地板,则需要自己安排(例如使用 np.floor())。在将复数转换为整数的情况下,没有合理的默认方法,因此 numpy 会引发异常并保持数组不变。

最后,还有两个稍微更技术性的问题。

如果您想以编程方式操作索引,您应该知道,当您编写类似以下内容时

In [14]
C[1,0,1]
Out[14]
13

它与以下内容相同(实际上它在内部被转换为以下内容)

In [15]
C[(1,0,1)]
Out[15]
13

这种看起来很奇怪的语法是构造一个元组,它是 Python 用于不可变序列的数据结构,并使用该元组作为数组的索引。(在幕后,C[1,0,1] 被转换为 C.__getitem__((1,0,1))。)这意味着您可以根据需要创建元组

In [16]
i = (1,0,1)
C[i]
Out[16]
13

如果看起来您永远不会想要这样做,请考虑遍历任意多维数组

In [17]
for i in np.ndindex(B.shape):
    print i, B[i]
(0, 0) 0
(0, 1) 1
(0, 2) 2
(1, 0) 3
(1, 1) 4
(1, 2) 5
(2, 0) 6
(2, 1) 7
(2, 2) 8

当我们开始研究花式索引和函数 np.where() 时,使用元组进行索引也会变得很重要。

我要提到的最后一个技术问题是,当你从数组中选择一个元素时,你得到的元素类型与数组元素的类型相同。这听起来可能很明显,在某种程度上确实如此,但请记住,即使像我们的 A、B 和 C 这样的无害 numpy 数组通常包含与 python 类型不完全相同的类型。

In [18]
a = C[1,2,3]
a
Out[18]
23
In [19]
type(a)
Out[19]
numpy.int64
In [20]
type(int(a))
Out[20]
int
In [21]
a**a
-c:1: RuntimeWarning: overflow encountered in long_scalars
Out[21]
8450172506621111015
In [22]
int(a)**int(a)
Out[22]
20880467999847912034355032910567L

为了保持一致性,numpy 标量也支持某些索引操作,但这些操作比较微妙,目前正在讨论中。

切片

显然,能够处理数组的单个元素至关重要。但 numpy 的一大卖点是能够进行“数组级”操作。

In [23]
2*A
Out[23]
array([ 0,  2,  4,  6,  8, 10, 12, 14, 16, 18])

这很方便,但人们经常只想处理数组的一部分。例如,假设你想计算 A 的差值数组,即元素为 A[1]-A[0]、A[2]-A[1] 等等的数组。(事实上,函数 np.diff 可以做到这一点,但为了方便说明,我们先忽略它。)numpy 使得使用数组级操作来实现这一点成为可能。

In [24]
A[1:]
Out[24]
array([1, 2, 3, 4, 5, 6, 7, 8, 9])
In [25]
A[:-1]
Out[25]
array([0, 1, 2, 3, 4, 5, 6, 7, 8])
In [26]
A[1:] - A[:-1]
Out[26]
array([1, 1, 1, 1, 1, 1, 1, 1, 1])

这是通过创建一个包含 A 中除第一个元素之外的所有元素的数组,一个包含 A 中除最后一个元素之外的所有元素的数组,然后减去相应的元素来实现的。以这种方式获取子数组的过程称为“切片”。

一维切片

切片的通用语法为 array[start:stop:step]。任何或所有值 startstopstep 都可以省略(如果省略了 step,前面的冒号也可以省略)。

In [27]
A[5:]
Out[27]
array([5, 6, 7, 8, 9])
In [28]
A[:5]
Out[28]
array([0, 1, 2, 3, 4])
In [29]
A[::2]
Out[29]
array([0, 2, 4, 6, 8])
In [30]
A[1::2]
Out[30]
array([1, 3, 5, 7, 9])
In [31]
A[1:8:2]
Out[31]
array([1, 3, 5, 7])

与 python 一样,start 索引包含在内,stop 索引不包含在内。同样,python 中的负数 startstop 从数组末尾反向计数。

In [32]
A[-3:]
Out[32]
array([7, 8, 9])
In [33]
A[:-3]
Out[33]
array([0, 1, 2, 3, 4, 5, 6])

如果stop在数组中出现在start之前,则返回长度为零的数组

In [34]
A[5:3]
Out[34]
array([], dtype=int64)

(“dtype=int32”出现在打印形式中,因为在没有元素的数组中,无法从其打印表示形式中判断元素的类型。尽管如此,跟踪它们在数组具有元素时的类型仍然是有意义的。)

如果指定的切片恰好只有一个元素,则返回的数组恰好只有一个元素

In [35]
A[5:6]
Out[35]
array([5])
In [36]
A[5]
Out[36]
5

这看起来相当明显且合理,但在处理花式索引和多维数组时,可能会令人惊讶。

如果数字step为负数,则遍历数组的步长为负数,也就是说,新数组包含(部分)原始数组中的元素,但顺序相反

In [37]
A[::-1]
Out[37]
array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])

这非常有用,但当给出startstop时可能会令人困惑

In [38]
A[5:3:-1]
Out[38]
array([5, 4])
In [39]
A[3:5:1]
Out[39]
array([3, 4])

要记住的规则是:无论step是正数还是负数,start始终包含,stop始终不包含。

就像可以将数组的元素作为子数组而不是逐个检索一样,也可以将它们作为子数组而不是逐个修改

In [ ]
#!python numbers=disable
>>> T = A.copy()
>>> T
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> T[1::2]
array([1, 3, 5, 7, 9])
>>> T[1::2] = -np.arange(5)
>>> T[1::2]
array([ 0, -1, -2, -3, -4])
>>> T
array([ 0,  0,  2, -1,  4, -2,  6, -3,  8, -4])

如果要分配的数组形状错误,则会引发异常

In [ ]
#!python numbers=disable
>>> T = A.copy()
>>> T[1::2] = np.arange(6)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: shape mismatch: objects cannot be broadcast to a single shape
>>> T[:4] = np.array([[0,1],[1,0]])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: shape mismatch: objects cannot be broadcast to a single shape

如果你认为错误消息听起来令人困惑,我不得不承认,但这是有原因的。在第一种情况下,我们试图将六个元素塞入五个插槽中,因此 numpy 拒绝了。在第二种情况下,元素数量正确 - 四个 - 但我们试图将一个二维数组塞入应该是一个长度为四的一维数组的位置。虽然 numpy 可以将二维数组强制转换为正确的形状,但设计者选择遵循 python 的哲学“显式优于隐式”,并将任何强制转换留给用户。让我们这样做,尽管

In [ ]
#!python numbers=disable
>>> T = A.copy()
>>> T[:4] = np.array([[0,1],[1,0]]).ravel()
>>> T
array([0, 1, 1, 0, 4, 5, 6, 7, 8, 9])

因此,为了使分配工作,仅仅拥有正确的元素数量是不够的 - 它们必须排列在具有正确形状的数组中。

还有一个问题使错误消息变得复杂:numpy 有一些非常方便的规则,用于将低维数组转换为高维数组,以及沿轴隐式重复数组。这个过程被称为“广播”。我们将在其他地方看到更多,但这里是最简单的形式

In [ ]
#!python numbers=disable
>>> T = A.copy()
>>> T[1::2] = -1
>>> T
array([ 0, -1,  2, -1,  4, -1,  6, -1,  8, -1])

我们告诉 NumPy 将一个标量 -1 放入一个长度为 5 的数组中。NumPy 的广播规则并没有报错,而是将这个标量转换为一个长度为 5 的有效数组,通过重复标量 5 次来实现。(当然,它实际上并没有创建这样一个临时数组;事实上,它使用了一个巧妙的技巧,告诉自己这个临时数组的元素间隔为 0 字节。)这种特殊的广播情况经常被使用。

In [ ]
#!python numbers=disable
>>> T = A.copy()
>>> T[1::2] -= 1
>>> T
array([0, 0, 2, 2, 4, 4, 6, 6, 8, 8])

赋值有时是使用“全部”切片的良好理由。

In [ ]
#!python numbers=disable
>>> T = A.copy()
>>> T[:] = -1
>>> T
array([-1, -1, -1, -1, -1, -1, -1, -1, -1, -1])
>>> T = A.copy()
>>> T = -1
>>> T
-1

这里发生了什么?好吧,在第一种情况下,我们告诉 NumPy 将 -1 赋值给 T 的所有元素,所以它就做了。在第二种情况下,我们告诉 Python “T = -1”。在 Python 中,变量只是可以附加到内存中对象的名称。这与 C 等语言形成鲜明对比,在 C 等语言中,变量是内存中存储数据的命名区域。对变量名(在本例中为 T)的赋值只是更改了名称所指向的对象,而不会以任何方式改变底层对象。(如果该名称是唯一指向原始对象的引用,则在重新赋值后,您的程序将无法再找到它,因此 Python 会删除原始对象以释放一些内存。)在像 C 这样的语言中,对变量的赋值会改变存储在该内存区域中的值。如果您真的必须用 C 的思维方式来思考,您可以将所有 Python 变量视为指向实际对象的指针;对 Python 变量的赋值只是修改指针,不会影响指向的对象(除非垃圾回收删除它)。无论如何,如果您想修改数组的内容,您不能通过对您给数组的名称进行赋值来实现;您必须使用切片赋值或其他方法。

最后,一个技术点:程序如何以编程方式使用切片?如果您想将切片规范保存起来以便稍后应用于多个数组,该怎么办?答案是使用切片对象,它使用 slice() 构造。

In [ ]
#!python numbers=disable
>>> A[1::2]
array([1, 3, 5, 7, 9])
>>> s = slice(1,None,2)
>>> A[s]
array([1, 3, 5, 7, 9])

(遗憾的是,您不能只写“s = 1::2”。但在方括号内,1::2 会在内部转换为 slice(1,None,2)。)您可以像使用冒号符号一样省略 slice() 的参数,只有一个例外。

In [ ]
#!python numbers=disable
>>> A[slice(-3)]
array([0, 1, 2, 3, 4, 5, 6])
>>> A[slice(None,3)]
array([0, 1, 2])
>>> A[slice()]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: slice expected at least 1 arguments, got 0
>>> A[slice(None,None,None)]
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

多维切片

一维数组非常有用,但通常人们会遇到本质上是多维的数据——例如,图像数据可能是 N×M 像素值的数组,或者 N×M×3 的颜色值数组。正如对一维数组进行切片很有用一样,对多维数组进行切片也很有用。这相当简单。

In [ ]
#!python numbers=disable
>>> B
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
>>> B[:2,:]
array([[0, 1, 2],
       [3, 4, 5]])
>>> B[:,::-1]
array([[2, 1, 0],
       [5, 4, 3],
       [8, 7, 6]])

本质上,你只需为每个轴指定一个一维切片。你也可以为某个轴提供一个数字,而不是切片。

In [ ]
#!python numbers=disable
>>> B[0,:]
array([0, 1, 2])
>>> B[0,::-1]
array([2, 1, 0])
>>> B[:,0]
array([0, 3, 6])

请注意,当你为(例如)第一个轴提供一个数字时,结果不再是二维数组;它现在是一维数组。这是有道理的。

In [ ]
#!python numbers=disable
>>> B[:,:]
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
>>> B[0,:]
array([0, 1, 2])
>>> B[0,0]
0

如果你不提供任何数字,你将得到一个二维数组;如果你提供一个数字,维度会下降一个,你将得到一个一维数组;如果你提供两个数字,维度会下降两个,你将得到一个标量。(如果你认为你应该得到一个零维数组,那么你正在打开一个潘多拉魔盒。标量和零维数组之间的区别,或缺乏区别,是一个正在讨论和开发的问题。)

如果你习惯于使用矩阵,你可能希望保留“行向量”和“列向量”之间的区别。numpy 只支持一种一维数组,但你可以将行向量和列向量表示为二维数组,其中一个维度恰好为一。不幸的是,这些对象的索引变得很麻烦。

与一维数组一样,如果你指定的切片恰好只有一个元素,你将得到一个数组,其中一个轴的长度为 1——该轴不会像你为该轴提供实际数字那样“消失”。

In [ ]
#!python numbers=disable
>>> B[:,0:1]
array([[0],
       [3],
       [6]])
>>> B[:,0]
array([0, 3, 6])

numpy 还有一些非常适合处理具有不确定维数的数组的快捷方式。如果这看起来像是不合理的事情,请记住,numpy 的许多函数(例如 np.sort()、np.sum() 和 np.transpose())必须在任意维度的数组上工作。当然,可以从数组中提取维度数并显式地使用它,但你的代码往往会充满类似 (slice(None,None,None),)*(C.ndim-1) 的东西,这使得代码难以阅读。因此,numpy 有一些快捷方式,通常可以简化事情。

首先是 Ellipsis 对象

In [ ]
#!python numbers=disable
>>> A[...]
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> B[...]
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
>>> B[0,...]
array([0, 1, 2])
>>> B[0,...,0]
array(0)
>>> C[0,...,0]
array([0, 4, 8])
>>> C[0,Ellipsis,0]
array([0, 4, 8])

省略号(三个点)表示“根据需要添加任意数量的冒号”。(它在索引操作代码中的名称是 Ellipsis,它不是 NumPy 特定的。)这使得只操作数组的一个维度变得容易,让 NumPy 在“不需要的”维度上执行数组操作。在任何给定的索引表达式中,你只能有一个省略号,否则表达式将对每个位置应该插入多少个冒号产生歧义。(事实上,出于某种原因,允许使用类似“C[...,...]”的表达式;这实际上并不模棱两可。)

在某些情况下,完全省略省略号很方便。

In [ ]
#!python numbers=disable
>>> B[0]
array([0, 1, 2])
>>> C[0]
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> C[0,0]
array([0, 1, 2, 3])
>>> B[0:2]
array([[0, 1, 2],
       [3, 4, 5]])

如果你没有为数组提供足够的索引,省略号会自动附加。这意味着在某种意义上,你可以将二维数组视为一维数组的数组。结合 NumPy 的数组操作,这意味着为一维数组编写的函数通常可以适用于二维数组。例如,回想一下我们在关于一维切片的部分中写出的差值操作。

In [ ]
#!python numbers=disable
>>> A[1:] - A[:-1]
array([1, 1, 1, 1, 1, 1, 1, 1, 1])
>>> B[1:] - B[:-1]
array([[3, 3, 3],
       [3, 3, 3]])

它可以不加修改地用于获取二维数组第一个轴上的差值。

写入多维切片的方式与写入一维切片的方式相同。

In [ ]
>>> T = B.copy()
>>> T[1,:] = -1
>>> T
array([[ 0,  1,  2],
       [-1, -1, -1],
       [ 6,  7,  8]])
>>> T[:,:2] = -2
>>> T
array([[-2, -2,  2],
       [-2, -2, -1],
       [-2, -2,  8]])

待办事项:np.newaxis 和广播规则。

视图与副本

待办事项:零维数组,单个元素的视图。

花式索引

切片非常方便,而且它们可以作为视图创建,这使得它们非常高效。但有些操作不能真正用切片完成;例如,假设想要对数组中所有负值进行平方。除了在 Python 中编写循环之外,还需要能够定位负值,提取它们,对它们进行平方,并将新值放在旧值的位置。

In [ ]
#!python numbers=disable
>>> T = A.copy() - 5
>>> T[T<0] **= 2
>>> T
array([25, 16,  9,  4,  1,  0,  1,  2,  3,  4])

或者假设想要使用数组作为查找表,也就是说,对于数组 B,生成一个数组,其第 i,j 个元素是 LUT[B[i,j]]:待办事项:argsort 是一个更好的例子。

In [ ]
#!python numbers=disable
>>> LUT = np.sin(A)
>>> LUT
array([ 0.        ,  0.84147098,  0.90929743,  0.14112001, -0.7568025 ,
       -0.95892427, -0.2794155 ,  0.6569866 ,  0.98935825,  0.41211849])
>>> LUT[B]
array([[ 0.        ,  0.84147098,  0.90929743],
       [ 0.14112001, -0.7568025 , -0.95892427],
       [-0.2794155 ,  0.6569866 ,  0.98935825]])

为了实现这种操作,NumPy 提供了所谓的“花式索引”。它不像切片那样快和轻量级,但它允许你做一些相当复杂的事情,同时让 NumPy 在 C 中完成所有繁重的工作。

布尔索引

人们经常想要选择或修改仅满足某些条件的数组元素。NumPy 提供了多种工具来处理这种情况。第一个是布尔数组。NumPy 数组之间的比较——等于、小于等等——会生成布尔值数组。

In [ ]
#!python numbers=disable
>>> A<5
array([ True,  True,  True,  True,  True, False, False, False, False, False], dtype=bool)

这些是普通的数组。实际的存储类型通常是每个值一个字节,而不是将位打包到一个字节中,但布尔数组提供了与其他数组相同的索引和数组操作范围。不幸的是,Python 的“and”和“or”不能被重写以执行数组操作,因此你必须使用按位操作符“&”、“|”和“\^”(用于异或)。类似地,Python 的链式不等式也不能被重写。此外,遗憾的是,你不能更改按位运算符的优先级。

In [ ]
#!python numbers=disable
>>> c = A<5 & A>1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
>>> c = (A<5) & (A>1)
>>> c
array([False, False,  True,  True,  True, False, False, False, False, False], dtype=bool)

尽管如此,NumPy 的布尔数组非常强大。

可以使用布尔数组从数组中提取值。

In [ ]
#!python numbers=disable
>>> c = (A<5) & (A>1)
>>> A[c]
array([2, 3, 4])

结果必然是原始数组的副本,而不是视图,因为通常情况下,c 中为 True 的元素不会选择均匀步长的内存布局。尽管如此,也可以使用布尔数组写入特定元素。

In [ ]
>>> T = A.copy()
>>> c = (A<5) & (A>1)
>>> T[c] = -7
>>> T
array([ 0,  1, -7, -7, -7,  5,  6,  7,  8,  9])

待办事项:提及 where()

多维布尔索引

布尔索引也适用于多维数组。在最简单(也是最常见)的情况下,只需提供一个与原始数组形状相同的布尔数组作为索引。

In [ ]
>>> C[C%5==0]
array([ 0,  5, 10, 15, 20])

然后你会得到一个包含满足条件为 True 的元素的一维数组。(注意,数组必须是一维的,因为布尔值可以在数组中任意排列。如果你想跟踪原始数组中值的排列,可以考虑使用 numpy 的“掩码数组”工具。)你也可以使用布尔索引进行赋值,就像你对一维数组那样。

布尔数组上两个非常有用的操作是 np.any() 和 np.all()

In [ ]
>>> np.any(B<5)
True
>>> np.all(B<5)
False

它们的功能正如其名,评估布尔矩阵中是否有任何条目为 True,或者布尔矩阵中所有元素是否都为 True。但它们也可以用于评估“沿轴”,例如,生成一个布尔数组,表示给定行中是否有任何元素为 True。

In [ ]
>>> B<5
array([[ True,  True,  True],
       [ True,  True, False],
       [False, False, False]], dtype=bool)
>>> np.any(B<5, axis=1)
array([ True,  True, False], dtype=bool)
>>> np.all(B<5, axis=1)
array([ True, False, False], dtype=bool)

也可以使用布尔索引提取满足某些条件的行或列。

In [ ]
>>> B[np.any(B<5, axis=1),:]
array([[0, 1, 2],
       [3, 4, 5]])

这里的结果是二维的,因为布尔索引的结果有一维,而每一行是一维的,所以还有一维。

这适用于更高维的布尔数组。

In [ ]
>>> c = np.any(C<5,axis=2)
>>> c
array([[ True,  True, False],
       [False, False, False]], dtype=bool)
>>> C[c,:]
array([[0, 1, 2, 3],
       [4, 5, 6, 7]])

这里的结果也是二维的,尽管这可能有点令人惊讶。布尔数组是二维的,但对应于布尔数组的返回值部分必须是一维的,因为 True 值可以任意分布。对应于每个 True 或 False 值的 C 的子数组是一维的,所以我们得到一个二维的返回值数组。

最后,如果你想同时对行和列应用布尔条件,请注意

In [ ]
>>> B[np.array([True, False, True]), np.array([False, True, True])]
array([1, 8])
>>> B[np.array([True, False, True]),:][:,np.array([False, True, True])]
array([[1, 2],
       [7, 8]])

显而易见的方法没有给出正确答案。我不知道为什么,也不知道为什么它会产生它产生的值。你可以通过两次索引来得到正确答案,但这很笨拙且效率低下,而且不允许赋值。

待办事项:由于某种原因,它适用于过小的布尔数组?

位置列表索引

经常需要从数组中提取特定位置的值。如果只需要一个位置,可以使用简单的索引。但如果有多个位置,就需要更巧妙的方法。幸运的是,numpy 支持一种花式索引模式来实现这一点。

In [ ]
>>> primes = np.array([2,3,5,7,11,13,17,19,23])
>>> idx = [3,4,1,2,2]
>>> primes[idx]
array([ 7, 11,  3,  5,  5])
>>> idx = np.array([3,4,1,2,2])
>>> primes[idx]
array([ 7, 11,  3,  5,  5])

当使用非布尔数组或列表进行索引时,numpy 会将其视为索引数组。该数组可以是任何形状,返回的数组具有相同的形状。

In [ ]
>>> primes = np.array([2,3,5,7,11,13,17,19,23,29,31])
>>> primes[B]
array([[ 2,  3,  5],
       [ 7, 11, 13],
       [17, 19, 23]])

实际上,这将原始数组用作查找表。

也可以用这种方式对数组进行赋值。

In [ ]
>>> T = A.copy()
>>> T[ [1,3,5,0] ] = -np.arange(4)
>>> T
array([-3,  0,  2, -1,  4, -2,  6,  7,  8,  9])

警告:增强赋值运算符(如 "+=")可以工作,但其结果可能并非如你预期的那样。特别是,重复索引不会导致值被添加两次。

In [ ]
>>> T = A.copy()
>>> T[ [0,1,2,3,3,3] ] += 10
>>> T
array([10, 11, 12, 13,  4,  5,  6,  7,  8,  9])

这令人惊讶、不便且不幸,但这是 Python 实现 "+=" 运算符的直接结果。最常见的用例是类似直方图的操作。

In [ ]
>>> bins = np.zeros(5,dtype=np.int32)
>>> pos = [1,0,2,0,3]
>>> wts = [1,2,1,1,4]
>>> bins[pos]+=wts
>>> bins
array([1, 1, 1, 4, 0])

不幸的是,这会给出错误的结果。在旧版本的 numpy 中,没有真正令人满意的解决方案,但从 numpy 1.1 开始,直方图函数可以做到这一点。

In [ ]
>>> bins = np.zeros(5,dtype=np.int32)
>>> pos = [1,0,2,0,3]
>>> wts = [1,2,1,1,4]
>>> np.histogram(pos,bins=5,range=(0,5),weights=wts,new=True)
(array([3, 1, 1, 4, 0]), array([ 0.,  1.,  2.,  3.,  4.,  5.]))

FIXME:提及 put() 和 take()

多维位置列表索引

毫不奇怪,也可以对多维数组使用位置列表索引。然而,语法有点令人惊讶。假设我们想要列表 [B[0,0],B[1,2],B[0,1]]。那么我们写

In [ ]
>>> B[ [0,1,0], [0,2,1] ]
array([0, 5, 1])
>>> [B[0,0],B[1,2],B[0,1]]
[0, 5, 1]

这可能看起来很奇怪 - 为什么不提供一个表示坐标的元组列表呢?原因是,对于大型数组来说,列表和元组效率非常低,因此 numpy 被设计为仅使用数组,包括索引和值。这意味着类似 B[ [(0,0),(1,2),(0,1)] ] 的操作看起来就像用二维数组索引 B 一样,正如我们上面所见,这仅仅意味着 B 应该用作查找表,生成一个二维结果数组(每个结果都是一维的,就像我们只对二维数组提供一个索引时一样)。

总之,在位置列表索引中,你为每个坐标提供一个形状相同的数组,numpy 返回一个具有相同形状的数组,其中包含通过在原始数组中查找每个坐标集获得的值。如果坐标数组的形状不同,numpy 的广播规则将应用于它们,以尝试使它们的形状相同。如果数组数量少于原始数组的维数,则原始数组被视为包含数组,并且额外的维数将出现在结果数组上。

幸运的是,大多数情况下,当需要为多维数组提供位置列表时,该列表本身就是从 numpy 中获得的。一种常见的做法是

In [ ]
>>> idx = np.nonzero(B%2)
>>> idx
(array([0, 1, 1, 2]), array([1, 0, 2, 1]))
>>> B[idx]
array([1, 3, 5, 7])
>>> B[B%2 != 0]
array([1, 3, 5, 7])

这里 nonzero() 函数接受一个数组并返回一个列表,其中包含数组中非零元素的位置(以正确的格式)。当然,也可以使用布尔数组直接索引数组;如果非零元素数量较少且索引操作执行多次,这将更加高效。但有时直接使用索引列表会更有价值。

选取行和列

NumPy 的位置列表索引语法的一个不幸结果是,习惯了其他数组语言的用户期望它能够选取行和列。毕竟,从矩阵中提取行和列列表是相当合理的。因此,NumPy 提供了一个方便的函数 ix_() 来执行此操作。

In [ ]
>>> B[ np.ix_([0,2],[0,2]) ]
array([[0, 2],
       [6, 8]])
>>> np.ix_([0,2],[0,2])
(array([[0],
       [2]]), array([[0, 2]]))

它的工作原理是利用 NumPy 的广播功能。您可以看到用作行和列索引的两个数组具有不同的形状;NumPy 的广播会在较短的轴上重复每个数组,以便它们一致。

混合索引模式

当您尝试混合切片索引、元素索引、布尔索引和位置列表索引时会发生什么?

索引的内部工作原理

NumPy 数组是一块内存、用于解释内存位置的数据类型、一个大小列表和一个步长列表。例如,C[i,j,k] 是从位置 i*strides[0]+j*strides[1]+k*strides[2] 开始的元素。这意味着,例如,可以非常高效地转置矩阵:只需反转步长和大小数组即可。这就是切片高效且可以返回视图的原因,而花式索引则较慢且无法做到。

在 Python 层面上,NumPy 的索引通过覆盖 ndarray 对象中的 __getitem__ 和 __setitem__ 方法来实现。当对数组进行索引时,会调用这些方法,它们允许任意实现。

In [ ]
>>> class IndexDemo:
...     def __getitem__(self, *args):
...         print "__getitem__", args
...         return 1
...     def __setitem__(self, *args):
...         print "__setitem__", args
...     def __iadd__(self, *args):
...         print "__iadd__", args
...
>>>
>>> T = IndexDemo()
>>> T[1]
__getitem__ (1,)
1
>>> T["fish"]
__getitem__ ('fish',)
1
>>> T[A]
__getitem__ (array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),)
1
>>> T[1,2]
__getitem__ ((1, 2),)
1
>>> T[1] = 7
__setitem__ (1, 7)
>>> T[1] += 7
__getitem__ (1,)
__setitem__ (1, 8)

类数组对象

NumPy 和 SciPy 提供了一些其他类型,它们的行为类似于数组,特别是矩阵和稀疏矩阵。它们的索引方式可能与数组的索引方式有很大不同。

章节作者:AMArchibald, jh