CQF预备知识:Python相关库 -- NumPy 基础知识 - ndarray 索引
文中内容仅限技术学习与代码实践参考,市场存在不确定性,技术分析需谨慎验证,不构成任何投资建议。
ndarray 索引
另请参阅 索引例程
ndarrays
可以使用标准的 Python x[obj]
语法进行索引,其中 x 是数组,obj 是选择器。根据 obj 的不同,有不同类型的索引方式:基本索引、高级索引和字段访问。
以下大多数示例展示了在引用数组中的数据时索引的使用。这些示例在为数组赋值时同样适用。有关赋值如何工作的具体示例和解释,请参阅“为索引数组赋值”部分。
请注意,在 Python 中,x[(exp1, exp2, ..., expN)]
等同于 x[exp1, exp2, ..., expN]
;后者只是前者的语法糖。
基本索引
单元素索引
单元素索引的工作方式与其他标准 Python 序列完全相同。它是从 0 开始的,并且接受负索引来从数组末尾进行索引。
>>> x = np.arange(10)
>>> x[2]
2
>>> x[-2]
8
没有必要将每个维度的索引分别放入自己的方括号中。
>>> x.shape = (2, 5) # 现在 x 是二维的
>>> x[1, 3]
8
>>> x[1, -1]
9
需要注意的是,如果用比数组维度少的索引去索引多维数组,就会得到一个次维数组。例如:
>>> x[0]
array([0, 1, 2, 3, 4])
也就是说,每个指定的索引选择的是剩余维度对应的数组。在上面的例子中,选择 0 意味着剩下的长度为 5 的维度没有被具体指定,返回的是一个具有该维度性和大小的数组。必须注意的是,返回的数组是一个视图,也就是说,它不是原始数组的副本,而是与原始数组在内存中指向相同的值。在这种情况下,返回的是位于第一个位置(0)的一维数组。因此,对返回的数组使用单个索引,结果会返回一个单个元素。也就是说:
x[0, 2] == x[0][2]
虽然第二种情况效率较低,因为第一次索引后会创建一个新的临时数组,然后该临时数组再被 2 索引。
注意
NumPy 使用 C 顺序索引。这意味着最后一个索引通常代表变化最快的内存位置,这与 Fortran 或 IDL 不同,在 Fortran 或 IDL 中,第一个索引代表内存中变化最快的位置。这种差异可能会引起极大的混淆。
切片和步长
基本切片将 Python 的基本切片概念扩展到了 N 维。当 obj 是一个 slice
对象(由方括号内的 start:stop:step
表示法构造)、一个整数,或者是一个包含 slice
对象和整数的元组时,就会发生基本切片。Ellipsis
和 newaxis
对象也可以与这些对象交错使用。
最简单的使用 N 个整数进行索引的情况会返回一个数组标量,表示对应的元素。与 Python 一样,所有索引都是基于零的:对于第 i-th 索引 n _ i n\_i n_i,有效的范围是 0 ≤ n _ i < d _ i 0 \le n\_i < d\_i 0≤n_i<d_i,其中 d _ i d\_i d_i 是数组形状的第 i-th 元素。负索引被解释为从数组末尾开始计数(i.e.,如果 n _ i < 0 n\_i < 0 n_i<0,则表示 n _ i + d _ i n\_i + d\_i n_i+d_i)。
所有由基本切片生成的数组始终是原始数组的视图。
注意
NumPy 切片创建的是视图,而不是像 Python 内置序列(如字符串、元组和列表)那样的副本。在从大数组中提取一个小部分且在提取后原始大数组不再有用的情况下,需要格外小心,因为提取的小部分包含对原始大数组的引用,而原始大数组的内存不会被释放,直到所有从它派生的数组都被垃圾回收。在这种情况下,建议显式调用 copy()
。
标准的序列切片规则适用于基本切片的每个维度(包括使用步长索引)。一些需要记住的有用概念包括:
- 基本切片语法是
i:j:k
,其中 i 是起始索引,j 是结束索引,k 是步长( k n e q 0 k\\neq0 kneq0)。这将选择(在相应维度中)索引值为 i、i + k、…、i + (m - 1) k 的 m 个元素,其中 m = q + ( r n e q 0 ) m = q + (r\\neq0) m=q+(rneq0),q 和 r 是将 j - i 除以 k 得到的商和余数:j - i = q k + r,使得 i + (m - 1) k < j。例如:
>>> x = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> x[1:7:2]
array([1, 3, 5])
- 负的 i 和 j 被解释为 n + i 和 n + j,其中 n 是相应维度中的元素数量。负的 k 会使步长朝向较小的索引。从上面的例子来看:
>>> x[-2:10]
array([8, 9])
>>> x[-3:3:-1]
array([7, 6, 5, 4])
- 假设 n 是正在切片的维度中的元素数量。那么,如果 i 没有给出,对于 k > 0 它默认为 0,对于 k < 0 它默认为 n - 1。如果 j 没有给出,对于 k > 0 它默认为 n,对于 k < 0 它默认为 -n-1。如果 k 没有给出,它默认为 1。注意
::
与:
相同,表示选择此轴上的所有索引。
从上面的例子来看:
>>> x[5:]
array([5, 6, 7, 8, 9])
- 如果选择元组中的对象数量少于 N,则后续维度假定为
:
。例如:
>>> x = np.array([[[1],[2],[3]], [[4],[5],[6]]])
>>> x.shape
(2, 3, 1)
>>> x[1:2]
array([[[4],[5],[6]]])
-
一个整数 i 返回的值与
i:i+1
相同,除了 返回对象的维度会减少 1。特别是,如果选择元组的第 p-th 元素是一个整数(其他所有条目都是:
),则返回的对应子数组的维度为 N - 1。如果 N = 1,则返回的对象是一个数组标量。这些对象在“标量”部分有解释。 -
如果选择元组的所有条目都是
:
,除了第 p-th 条目是一个切片对象i:j:k
,那么返回的数组的维度为 N,通过沿第 p-th 轴堆叠,由整数索引的元素 i、i+k、…、i + (m - 1) k < j 返回的子数组形成。 -
基本切片中,如果切片元组中有多个非-
:
条目,其行为类似于对单个非-:
条目重复应用切片,非-:
条目依次被采用(所有其他非-:
条目被替换为:
)。因此,x[ind1, ..., ind2,:]
在基本切片下表现得像x[ind1][..., ind2, :]
。
警告
上述情况对于高级索引 不成立。
-
您可以使用切片在数组中设置值,但(与列表不同)您永远无法扩展数组。在
x[obj] = value
中要设置的值的大小必须(可广播到)与x[obj]
的形状相同。 -
切片元组总是可以构造为 obj 并在
x[obj]
表示法中使用。切片对象可以在构造中代替[start:stop:step]
表示法使用。例如,x[1:10:5, ::-1]
也可以通过obj = (slice(1, 10, 5), slice(None, None, -1)); x[obj]
来实现。这对于编写适用于任意维度数组的通用代码很有用。有关更多信息,请参阅“在程序中处理可变数量的索引”。
维度索引工具
有一些工具可以方便地匹配数组形状与表达式,并在赋值中使用。
Ellipsis
会扩展为所需数量的 :
对象,以便选择元组能够索引所有维度。在大多数情况下,这意味着扩展后的选择元组的长度是 x.ndim
。元组中只能出现一个 Ellipsis
。例如:
>>> x[..., 0]
array([[1, 2, 3],[4, 5, 6]])
这等同于:
>>> x[:, :, 0]
array([[1, 2, 3],[4, 5, 6]])
每个 newaxis
对象在选择元组中会扩展结果选择的维度,增加一个长度为单位的维度。新增的维度位置是 newaxis
对象在选择元组中的位置。newaxis
是 None
的别名,可以用 None
替代它,效果相同。例如:
>>> x[:, np.newaxis, :, :].shape
(2, 1, 3, 1)
>>> x[:, None, :, :].shape
(2, 1, 3, 1)
这可以方便地组合两个数组,否则需要显式的重塑操作。例如:
>>> x = np.arange(5)
>>> x[:, np.newaxis] + x[np.newaxis, :]
array([[0, 1, 2, 3, 4],[1, 2, 3, 4, 5],[2, 3, 4, 5, 6],[3, 4, 5, 6, 7],[4, 5, 6, 7, 8]])
高级索引
高级索引在选择对象 obj 是非元组序列对象、ndarray
(数据类型为整数或布尔值),或者是一个元组且至少包含一个序列对象或 ndarray
(数据类型为整数或布尔值)时被触发。有两种类型的高级索引:整数索引和布尔索引。
高级索引总是返回数据的 副本(与基本切片返回视图形成对比)。
警告
高级索引的定义意味着 x[(1, 2, 3),]
与 x[(1, 2, 3)]
有根本的不同。后者等同于 x[1, 2, 3]
,将触发基本选择,而前者将触发高级索引。务必理解为什么会发生这种情况。
整数数组索引
整数数组索引允许根据其 N-维索引选择数组中的任意项。每个整数数组代表该维度中的一组索引。
在索引数组中允许使用负值,其工作方式与单个索引或切片中的负值相同:
>>> x = np.arange(10, 1, -1)
>>> x
array([10, 9, 8, 7, 6, 5, 4, 3, 2])
>>> x[np.array([3, 3, 1, 8])]
array([7, 7, 9, 2])
>>> x[np.array([3, 3, -3, 8])]
array([7, 7, 4, 2])
如果索引值超出范围,则会抛出 IndexError
:
>>> x = np.array([[1, 2], [3, 4], [5, 6]])
>>> x[np.array([1, -1])]
array([[3, 4],[5, 6]])
>>> x[np.array([3, 4])]
Traceback (most recent call last):...
IndexError: index 3 is out of bounds for axis 0 with size 3
当索引由与被索引数组维度一样多的整数数组组成时,索引过程是直接的,但与切片不同。
高级索引总是会广播并迭代为 一个整体:
result[i_1, ..., i_M] == x[ind_1[i_1, ..., i_M], ind_2[i_1, ..., i_M],..., ind_N[i_1, ..., i_M]]
请注意,结果的形状与索引数组的形状(ind_1, ..., ind_N
)一致。如果索引数组不能广播为相同的形状,则会引发异常 IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes...
。
使用多维索引数组进行索引的情况较为少见,但也是被允许的,并且在某些问题中很有用。我们先从最简单的多维情况开始:
>>> y = np.arange(35).reshape(5, 7)
>>> y
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, 24, 25, 26, 27],[28, 29, 30, 31, 32, 33, 34]])
>>> y[np.array([0, 2, 4]), np.array([0, 1, 2])]
array([ 0, 15, 30])
在这种情况下,如果索引数组的形状匹配,并且每个维度都有一个索引数组,则结果数组的形状与索引数组的形状相同,值对应于索引数组中每个位置的索引集。在这个例子中,第一个索引值在两个索引数组中都是 0,因此结果数组的第一个值是 y[0, 0]
。接下来的值是 y[2, 1]
,最后一个值是 y[4, 2]
。
如果索引数组的形状不相同,会尝试将它们广播为相同的形状。如果无法广播为相同的形状,则会引发异常:
>>> y[np.array([0, 2, 4]), np.array([0, 1])]
Traceback (most recent call last):...
IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes (3,) (2,)
广播机制允许索引数组与标量索引结合使用。其效果是标量值用于索引数组的所有对应值:
>>> y[np.array([0, 2, 4]), 1]
array([ 1, 15, 29])
接下来,我们进入更复杂的层面,可以只用索引数组部分地索引数组。理解这种情况需要一些思考。例如,如果只用一个索引数组来索引 y
:
>>> y[np.array([0, 2, 4])]
array([[ 0, 1, 2, 3, 4, 5, 6],[14, 15, 16, 17, 18, 19, 20],[28, 29, 30, 31, 32, 33, 34]])
结果是构建了一个新数组,其中索引数组的每个值选择被索引数组的一行,结果数组的形状为(索引元素的数量,行的大小)。
一般来说,结果数组的形状将是索引数组的形状(或者所有索引数组广播后的形状)与被索引数组中未使用的维度(那些未被索引的维度)的形状的拼接。
示例
从每一行中选择一个特定的元素。行索引只是 [0, 1, 2]
,而列索引指定了对应行中要选择的元素,这里为 [0, 1, 0]
。使用两者结合,可以通过高级索引完成这项任务:
>>> x = np.array([[1, 2], [3, 4], [5, 6]])
>>> x[[0, 1, 2], [0, 1, 0]]
array([1, 4, 5])
为了实现类似于上面的基本切片的行为,可以使用广播。ix_
函数可以帮助实现这种广播。这通过一个示例最容易理解。
示例
从一个 4×3 的数组中,使用高级索引选择角落元素。因此,所有列索引为 [0, 2]
且行索引为 [0, 3]
的元素都需要被选择。要使用高级索引,需要显式地选择所有元素。按照前面解释的方法,可以这样写:
>>> x = np.array([[ 0, 1, 2],
... [ 3, 4, 5],
... [ 6, 7, 8],
... [ 9, 10, 11]])
>>> rows = np.array([[0, 0],
... [3, 3]], dtype=np.intp)
>>> columns = np.array([[0, 2],
... [0, 2]], dtype=np.intp)
>>> x[rows, columns]
array([[ 0, 2],[ 9, 11]])
然而,由于上面的索引数组只是重复自身,可以使用广播(与操作 rows[:, np.newaxis] + columns
进行比较)来简化这个过程:
>>> rows = np.array([0, 3], dtype=np.intp)
>>> columns = np.array([0, 2], dtype=np.intp)
>>> rows[:, np.newaxis]
array([[0],[3]])
>>> x[rows[:, np.newaxis], columns]
array([[ 0, 2],[ 9, 11]])
这种广播也可以通过 ix_
函数实现:
>>> x[np.ix_(rows, columns)]
array([[ 0, 2],[ 9, 11]])
请注意,如果没有 np.ix_
调用,只有对角线元素会被选中:
>>> x[rows, columns]
array([ 0, 11])
这种差异是使用多个高级索引时最需要记住的重要内容。
示例
高级索引在实际应用中的一个例子是用于颜色查找表,我们希望将图像的值映射到 RGB 三元组中以便显示。查找表的形状可以是 (nlookup, 3)
。使用形状为 (ny, nx)
且数据类型为 np.uint8
(或任何整数类型,只要值在查找表的范围内)的图像来索引这样一个数组,将得到一个形状为 (ny, nx, 3)
的数组,其中每个像素位置都关联一个 RGB 三元组。
布尔数组索引
当 obj 是一个布尔类型的数组对象时,会发生这种高级索引,例如通过比较运算符返回的结果。一个布尔索引数组实际上与 x[obj.nonzero()]
几乎相同,如上所述,obj.nonzero()
返回一个长度为 obj.ndim
的整数索引数组元组,显示 obj 中的 True
元素。然而,当 obj.shape == x.shape
时,它运行得更快。
如果 obj.ndim == x.ndim
,x[obj]
返回一个一维数组,其中填充了 x 中对应于 obj 中 True
值的元素。搜索顺序将是按行优先的 C 风格。如果布尔索引数组的形状与 x 的对应维度不匹配,无论这些值是 True
还是 False
,都会引发索引错误。
这种用法的一个常见场景是筛选出期望的元素值。例如,可能需要选择数组中所有不是 numpy.nan
的条目:
>>> x = np.array([[1., 2.], [np.nan, 3.], [np.nan, np.nan]])
>>> x[~np.isnan(x)]
array([1., 2., 3.])
或者希望为所有负数元素加上一个常数:
>>> x = np.array([1., -1., -2., 3])
>>> x[x < 0] += 20
>>> x
array([ 1., 19., 18., 3.])
一般来说,如果索引中包含布尔数组,结果将与在相同位置插入 obj.nonzero()
并使用上面描述的整数数组索引机制完全相同。x[ind_1, boolean_array, ind_2]
等价于 x[(ind_1,) + boolean_array.nonzero() + (ind_2,)]
。
如果只有一个布尔数组且没有整数索引数组存在,这是很直接的。只需要确保布尔索引具有 恰好 与其将要工作的维度一样多的维度即可。
一般来说,当布尔数组的维度少于被索引数组的维度时,这等同于 x[b, ...]
,这意味着先用 b 索引 x,然后再加上尽可能多的 :
来填满 x 的秩。因此,结果的形状是一个维度包含布尔数组中 True
元素的数量,后面跟着被索引数组的剩余维度:
>>> x = np.arange(35).reshape(5, 7)
>>> b = x > 20
>>> b[:, 5]
array([False, False, False, True, True])
>>> x[b[:, 5]]
array([[21, 22, 23, 24, 25, 26, 27],[28, 29, 30, 31, 32, 33, 34]])
在这里,第 4 行和第 5 行被选中并组合成一个二维数组。
示例
从数组中选择所有行,这些行的和小于或等于二:
>>> x = np.array([[0, 1], [1, 1], [2, 2]])
>>> rowsum = x.sum(-1)
>>> x[rowsum <= 2, :]
array([[0, 1],[1, 1]])
示例
使用布尔索引选择所有行,这些行的和为偶数。同时,使用高级整数索引选择第 0 列和第 2 列。通过 ix_
函数可以实现:
>>> x = np.array([[ 0, 1, 2],
... [ 3, 4, 5],
... [ 6, 7, 8],
... [ 9, 10, 11]])
>>> rows = (x.sum(-1) % 2) == 0
>>> rows
array([False, True, False, True])
>>> columns = [0, 2]
>>> x[np.ix_(rows, columns)]
array([[ 3, 5],[ 9, 11]])
如果没有使用 np.ix_
调用,只有对角线元素会被选中。
或者不使用 np.ix_
(与整数数组示例进行比较):
>>> rows = rows.nonzero()[0]
>>> x[rows[:, np.newaxis], columns]
array([[ 3, 5],[ 9, 11]])
示例
使用一个形状为 (2, 3) 的二维布尔数组,其中包含四个 True
元素,从一个形状为 (2, 3, 5) 的三维数组中选择行,结果是一个形状为 (4, 5) 的二维数组:
>>> x = np.arange(30).reshape(2, 3, 5)
>>> x
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, 24],[25, 26, 27, 28, 29]]])
>>> b = np.array([[True, True, False], [False, True, True]])
>>> x[b]
array([[ 0, 1, 2, 3, 4],[ 5, 6, 7, 8, 9],[20, 21, 22, 23, 24],[25, 26, 27, 28, 29]])
高级索引与基本索引的结合
当索引中至少包含一个切片(:
)、省略号(...
)或 newaxis
,或者数组的维度多于高级索引的数量时,行为可能会更加复杂。这类似于对每个高级索引元素的索引结果进行拼接。
在最简单的情况下,只有一个高级索引与切片结合。例如:
>>> y = np.arange(35).reshape(5, 7)
>>> y[np.array([0, 2, 4]), 1:3]
array([[ 1, 2],[15, 16],[29, 30]])
实际上,切片和索引数组操作是独立的。切片操作提取索引为 1 和 2 的列(即第 2 列和第 3 列),然后索引数组操作提取索引为 0、2 和 4 的行(即第 1 行、第 3 行和第 5 行)。这等同于:
>>> y[:, 1:3][np.array([0, 2, 4]), :]
array([[ 1, 2],[15, 16],[29, 30]])
一个单独的高级索引可以替代切片,结果数组将相同。然而,它是一个副本,可能具有不同的内存布局。如果可能的话,切片是首选。
例如:
>>> x = np.array([[ 0, 1, 2],
... [ 3, 4, 5],
... [ 6, 7, 8],
... [ 9, 10, 11]])
>>> x[1:2, 1:3]
array([[4, 5]])
>>> x[1:2, [1, 2]]
array([[4, 5]])
理解多个高级索引组合的最简单方法可能是从结果的形状来考虑。索引操作分为两部分:基本索引(不包括整数)定义的子空间和高级索引部分定义的子空间。需要区分两种索引组合的情况:
-
高级索引被切片、省略号或
newaxis
分隔。例如x[arr1, :, arr2]
。 -
所有高级索引都紧挨在一起。例如
x[..., arr1, arr2, :]
,但不是x[arr1, :, 1]
,因为在这种情况下,1 是一个高级索引。
在第一种情况下,高级索引操作产生的维度会出现在结果数组的前面,子空间维度随后。
在第二种情况下,高级索引操作产生的维度会插入到结果数组中,位置与它们在初始数组中的位置相同(这种逻辑使得简单的高级索引表现得就像切片一样)。
示例
假设 x.shape
是 (10, 20, 30),ind
是一个形状为 (2, 5, 2) 的索引 intp
数组,那么 result = x[..., ind, :]
的形状将是 (10, 2, 5, 2, 30),因为 (20,)-形状的子空间被 (2, 5, 2)-形状的广播索引子空间替换了。如果让 i, j, k 遍历 (2, 5, 2)-形状的子空间,那么 result[..., i, j, k, :] = x[..., ind[i, j, k], :]
。这个例子产生了一个与 x.take(ind, axis=-2)
相同的结果。
示例
假设 x.shape
是 (10, 20, 30, 40, 50),并且假设 ind_1
和 ind_2
可以广播到形状 (2, 3, 4)。那么 x[:, ind_1, ind_2]
的形状是 (10, 2, 3, 4, 40, 50),因为来自 X 的 (20, 30)-形状的子空间被来自索引的 (2, 3, 4) 子空间替换了。然而,x[:, ind_1, :, ind_2]
的形状是 (2, 3, 4, 10, 30, 50),因为没有一个明确的地方可以插入索引子空间,因此它被附加到了开头。总是可以使用 .transpose()
将子空间移动到任何期望的位置。请注意,这个例子不能使用 take
来复制。
示例
切片可以与广播的布尔索引结合使用:
>>> x = np.arange(35).reshape(5, 7)
>>> b = x > 20
>>> b
array([[False, False, False, False, False, False, False],[False, False, False, False, False, False, False],[False, False, False, False, False, False, False],[ True, True, True, True, True, True, True],[ True, True, True, True, True, True, True]])
>>> x[b[:, 5], 1:3]
array([[22, 23],[29, 30]])
字段访问
如果 ndarray
对象是一个结构化数组,可以通过字符串索引数组的字段,就像字典一样。
索引 x['field-name']
返回对数组的新视图,该视图与 x 的形状相同(除非字段是一个子数组),但数据类型为 x.dtype['field-name']
,并且只包含指定字段中的数据部分。此外,记录数组标量也可以这样“索引”。
从结构化数组中索引也可以使用字段名列表,例如 x[['field-name1', 'field-name2']]
。从 NumPy 1.16 开始,这返回一个只包含这些字段的视图。在 NumPy 的旧版本中,它返回一个副本。有关多字段索引的更多信息,请参阅用户指南中的“结构化数组”部分。
如果访问的字段是一个子数组,子数组的维度将附加到结果的形状中。
例如:
>>> x = np.zeros((2, 2), dtype=[('a', np.int32), ('b', np.float64, (3, 3))])
>>> x['a'].shape
(2, 2)
>>> x['a'].dtype
dtype('int32')
>>> x['b'].shape
(2, 2, 3, 3)
>>> x['b'].dtype
dtype('float64')
平坦迭代器索引
x.flat
返回一个迭代器,将按 C 连续风格(最后一个索引变化最快)迭代整个数组。这个迭代器对象也可以使用基本切片或高级索引进行索引,只要选择对象不是元组即可。从 x.flat
是一维视图这一事实可以清楚地看出。它可以用于整数索引,使用一维 C 风格的平坦索引。因此,任何返回数组的形状将是整数索引对象的形状。
为索引数组赋值
如前所述,可以使用单个索引、切片以及索引和掩码数组选择数组的子集进行赋值。要赋值给索引数组的值必须与索引产生的形状一致(具有相同的形状或可广播到该形状)。例如,允许将常量赋值给切片:
>>> x = np.arange(10)
>>> x[2:7] = 1
或者赋值一个合适大小的数组:
>>> x[2:7] = np.arange(5)
请注意,赋值可能会导致类型提升(例如从浮点数赋值给整数)甚至引发异常(例如从复数赋值给浮点数或整数):
>>> x[1] = 1.2
>>> x[1]
1
>>> x[1] = 1.2j
Traceback (most recent call last):...
TypeError: can't convert complex to int
与某些引用(例如数组和掩码索引)不同,赋值总是对原始数据进行的(实际上,其他情况没有任何意义!)。请注意,尽管如此,某些操作可能不会像人们天真地期望的那样工作。这个特定的例子通常会让人们感到惊讶:
>>> x = np.arange(0, 50, 10)
>>> x
array([ 0, 10, 20, 30, 40])
>>> x[np.array([1, 1, 3, 1])] += 1
>>> x
array([ 0, 11, 20, 31, 40])
人们期望第 1 个位置会增加 3。实际上,它只会增加 1。原因是,从原始数组中提取了一个新数组(作为临时数组),包含索引为 1、1、3、1 的值,然后值 1 被加到临时数组上,然后临时数组被赋值回原始数组。因此,x[1] + 1
的值被赋值给 x[1]
三次,而不是将 x[1]
增加 3 次。
在程序中处理可变数量的索引
索引语法非常强大,但在处理可变数量的索引时会受到限制。例如,如果您想编写一个可以处理具有不同数量维度的参数的函数,而无需为可能的每个维度数量编写特殊代码,那么该如何实现呢?如果向索引提供一个元组,该元组将被解释为索引列表。例如:
>>> z = np.arange(81).reshape(3, 3, 3, 3)
>>> indices = (1, 1, 1, 1)
>>> z[indices]
40
因此,可以使用代码构建任意数量索引的元组,然后在索引中使用这些元组。
可以通过在 Python 中使用 slice()
函数来指定程序中的切片。例如:
>>> indices = (1, 1, 1, slice(0, 2)) # 等同于 [1, 1, 1, 0:2]
>>> z[indices]
array([39, 40])
同样,可以通过代码使用 Ellipsis
对象来指定省略号:
>>> indices = (1, Ellipsis, 1) # 等同于 [1, ..., 1]
>>> z[indices]
array([[28, 31, 34],[37, 40, 43],[46, 49, 52]])
由于元组的特殊处理,它们不会像列表那样自动转换为数组。例如:
>>> z[[1, 1, 1, 1]] # 产生一个较大的数组
array([[[[27, 28, 29],[30, 31, 32], ...
>>> z[(1, 1, 1, 1)] # 返回一个单一值
40
因为元组不会自动转换为数组,所以 np.nonzero()
函数的输出可以直接用作索引,因为它始终返回一个索引数组元组。
由于元组的特殊处理,它们不会像列表那样自动转换为数组。例如:
>>> z[[1, 1, 1, 1]] # 产生一个较大的数组
array([[[[27, 28, 29],[30, 31, 32], ...
>>> z[(1, 1, 1, 1)] # 返回一个单一值
40
详细说明
这些是一些详细的说明,对于日常索引来说并不重要(无特定顺序):
-
NumPy 的原生索引类型是
intp
,可能与默认的整数数组类型不同。intp
是足够安全地索引任何数组的最小数据类型;对于高级索引,它可能比其他类型更快。 -
对于高级赋值,通常没有保证迭代顺序。这意味着如果一个元素被设置多次,无法预测最终结果。
-
一个空的(元组)索引是一个完整的标量索引,进入一个零维数组。
x[()]
如果x
是零维的则返回一个 标量,否则返回一个视图。
另一方面,x[...]
总是返回一个视图。 -
如果索引中存在一个零维数组,并且它是一个完整的整数索引,则结果将是一个 标量,而不是一个零维数组。
(不会触发高级索引。) -
当省略号(
...
)存在但没有大小(即替换零个:
)时,结果将始终是一个数组。如果没有高级索引,则是一个视图,否则是一个副本。 -
对于布尔数组,
nonzero
等价性不适用于零维布尔数组。 -
当高级索引操作的结果没有元素,但单个索引超出范围时,是否引发
IndexError
是未定义的(例如x[[], [123]]
,其中123
超出范围)。 -
当在赋值过程中发生类型转换错误(例如使用字符串序列更新数值数组)时,正在被赋值的数组可能会最终处于一个不可预测的部分更新状态。
然而,如果发生任何其他错误(例如索引超出范围),数组将保持不变。 -
高级索引结果的内存布局针对每个索引操作进行了优化,不能假设任何特定的内存顺序。
-
当使用子类(特别是操纵其形状的子类)时,默认的
ndarray.__setitem__
行为将为 基本 索引调用__getitem__
,但不会为高级索引调用。对于这样的子类,可能更倾向于使用基础类ndarray
视图调用ndarray.__setitem__
。如果子类的__getitem__
不返回视图,则 必须 这样做。
风险提示与免责声明
本文内容基于公开信息研究整理,不构成任何形式的投资建议。历史表现不应作为未来收益保证,市场存在不可预见的波动风险。投资者需结合自身财务状况及风险承受能力独立决策,并自行承担交易结果。作者及发布方不对任何依据本文操作导致的损失承担法律责任。市场有风险,投资须谨慎。