当在numpy中传递列表而不是元组时进行高级切片

6
在文档中,它说(我强调):
高级索引会在选择对象obj是非元组序列对象、ndarray(整数或bool类型的数据)或至少有一个序列对象或ndarray(整数或bool类型的数据)的元组时触发。高级索引有两种类型:整数和布尔。
此外,请注意,x[[1,2,3]]将触发高级索引,而x[[1,2,slice(None)]]将触发基本切片。
我知道为什么x[(1, 2, slice(None))]会触发基本切片。但为什么x[[1,2,slice(None)]]会触发基本切片,当[1,2,slice(None)]满足非元组序列的条件?
相关的是,为什么会发生以下情况?
>>> a = np.eye(4)
>>> a[(1, 2)]  # basic indexing, as expected
0.0
>>> a[(1, np.array(2))] # basic indexing, as expected
0.0

>>> a[[1, 2]]  # advanced indexing, as expected
array([[ 0.,  1.,  0.,  0.],
   [ 0.,  0.,  1.,  0.]])
>>> a[[1, np.array(2)]]  # basic indexing!!??
0.0

1
毫无疑问,秘密就在源代码的某个地方。 - Eric
在当前的文档中,最后引用的那一行已经被修改为引用正在逐步淘汰的遗留代码。https://numpy.org/doc/stable/user/basics.indexing.html#slicing-and-striding。 - hpaulj
3个回答

9

这个规则有一个例外。高级索引文档部分没有提到它,但在基本切片和索引部分的开头附近,您会看到以下文字:

为了保持与Numeric中常见用法的向后兼容性,如果选择对象是任何非ndarray序列(如列表)包含切片对象、省略号对象或newaxis对象,则也会启动基本切片,但不适用于整数数组或其他嵌入序列。


a[[1, np.array(2)]]并不能完全触发基本索引。它会触发向后兼容逻辑的一个未记录部分,正如源代码中评论所描述:

    /*
     * Sequences < NPY_MAXDIMS with any slice objects
     * or newaxis, Ellipsis or other arrays or sequences
     * embedded, are considered equivalent to an indexing
     * tuple. (`a[[[1,2], [3,4]]] == a[[1,2], [3,4]]`)
     */

列表中的np.array(2)使得该列表被视为元组,但结果a[(1, np.array(2))]仍然是高级索引操作。它最终将12应用于不同的轴,与a[[1, 2]]不同,结果看起来与a[1, 2]相同,但如果你尝试使用一个3D的a,它会产生一个副本而不是视图。


这份文档仍然不完整,因为 a[[1, np.array(2)]] 触发了基本索引。 - Eric
1
不要开玩笑:https://ideone.com/3jXIjz。仅仅因为你认为它不应该有所不同,并不意味着它就是这样! - Eric
1
等一下,您是对的,这确实是一个奇怪的交互。我之前想到1np.array(2)作为单独轴上的高级索引,但它们应该沿同一轴。 - user2357112
hpaulj 指出源代码中的注释似乎表明确实是这种情况。列表内部的数组会导致将列表视为元组,但结果仍然是由于数组而进行高级索引。这种特殊情况似乎没有记录。 - user2357112
1
它生成的是副本而不是视图,这是否可以被认为是一个错误呢?通常情况下,尽可能生成视图是可取的,对吧? - Eric
显示剩余5条评论

2

使用一个虚拟类,我可以确定解释器如何将[...]转换为对__getitem__的调用。

In [1073]: class Foo():
      ...:     def __getitem__(idx):
      ...:         print(idx)
In [1080]: Foo()[1,2,slice(None)]
(1, 2, slice(None, None, None))
In [1081]: Foo()[(1,2,slice(None))]
(1, 2, slice(None, None, None))
In [1082]: Foo()[[1,2,slice(None)]]
[1, 2, slice(None, None, None)]

使用()将多个术语包装起来没有任何区别——在两种情况下都会得到一个元组。而列表作为列表传递。

因此,元组和列表(或不是)之间的区别必须在numpy源代码中编码——这是编译的。因此我不能轻易地研究它。

对于一维数组

使用列表进行索引会产生高级索引——选择特定的值:

In [1085]: arr[[1,2,3]]
Out[1085]: array([ 0.73703368,  0.        ,  0.        ])

但是,如果将其中一个值替换为元组或切片:

In [1086]: arr[[1,2,(2,3)]]
IndexError: too many indices for array

In [1088]: arr[[1,2,slice(None)]] 
IndexError: too many indices for array

列表被视为元组-它尝试将值与维度进行匹配。

因此,在顶层上,列表和元组被视为相同的 - 如果列表不能解释为高级索引列表。

还要注意单项列表之间的差异。

In [1089]: arr[[1]]
Out[1089]: array([ 0.73703368])
In [1090]: arr[(1,)]
Out[1090]: 0.73703367969998546
In [1091]: arr[1]
Out[1091]: 0.73703367969998546

一些函数(例如np.apply_along/over_axis)会生成索引列表或数组,然后对其进行操作。它们使用列表或数组是因为它们是可变的。有些函数在使用索引前会将其包装成元组,而有些则没有。这种差异让我有点困扰,但这些测试用例表明,这种包装成元组的做法通常是可选的。
In [1092]: idx=[1,2,slice(None)]
In [1093]: np.ones((2,3,4))[idx]
Out[1093]: array([ 1.,  1.,  1.,  1.])
In [1094]: np.ones((2,3,4))[tuple(idx)]
Out[1094]: array([ 1.,  1.,  1.,  1.])

如果我将索引构建为对象数组,似乎仍然需要元组包装器:

In [1096]: np.ones((2,3,4))[np.array(idx)]
...
IndexError: arrays used as indices must be of integer (or boolean) type
In [1097]: np.ones((2,3,4))[tuple(np.array(idx))]
Out[1097]: array([ 1.,  1.,  1.,  1.])

===================

来自函数@Eric的评论

    /*
     * Sequences < NPY_MAXDIMS with any slice objects
     * or newaxis, Ellipsis or other arrays or sequences
     * embedded, are considered equivalent to an indexing
     * tuple. (`a[[[1,2], [3,4]]] == a[[1,2], [3,4]]`)
     */

===================

此函数将对象数组和列表包装成元组以进行索引:

def apply_along_axis(func1d, axis, arr, *args, **kwargs):
     ....
     ind = [0]*(nd-1)
     i = zeros(nd, 'O')
     ....
     res = func1d(arr[tuple(i.tolist())], *args, **kwargs)
     outarr[tuple(ind)] = res

更新

现在,此列表索引会产生一个FutureWarning警告:

In [113]: arr.shape
Out[113]: (2, 3, 4)
In [114]: arr[[1, 2, slice(None)]]
<ipython-input-114-f30c20184e42>:1: FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated; use `arr[tuple(seq)]` instead of `arr[seq]`. In the future this will be interpreted as an array index, `arr[np.array(seq)]`, which will result either in an error or a different result.
  arr[[1, 2, slice(None)]]
Out[114]: array([20, 21, 22, 23])

将列表改为元组可以得到相同的结果,而且不会出现警告信息:
In [115]: arr[(1, 2, slice(None))]
Out[115]: array([20, 21, 22, 23])

这和以下内容是一样的:

In [116]: arr[1, 2, :]
Out[116]: array([20, 21, 22, 23])

使用逗号进行索引会创建一个元组,该元组将传递给__setitem__方法。

警告说,在未来它将尝试将列表转换为数组而不是元组:

In [117]: arr[np.array([1, 2, slice(None)])]
Traceback (most recent call last):
  Input In [117] in <module>
    arr[np.array([1, 2, slice(None)])]
IndexError: arrays used as indices must be of integer (or boolean) type

但使用slice对象会引发错误。因此,arr[tuple([....])]的解释是唯一有意义的。但这是一个遗留问题,来自早期的numeric包。

幸运的是,新手程序员不太可能尝试这样做。他们可能会尝试arr[[1,2,:]],但那会导致语法错误。冒号:只允许在索引括号中使用,而不允许在列表括号(或元组())中使用。

这些评论的当前轮次是由一个不同的情况触发的,该情况产生了FutureWarning:

In [123]: arr[[[0, 1], [1, 0]]]
<ipython-input-123-4fa43c8569dd>:1: FutureWarning: Using a non-tuple sequence for multidimensional indexing is deprecated; use `arr[tuple(seq)]` instead of `arr[seq]`. In the future this will be interpreted as an array index, `arr[np.array(seq)]`, which will result either in an error or a different result.
  arr[[[0, 1], [1, 0]]]
Out[123]: 
array([[ 4,  5,  6,  7],
       [12, 13, 14, 15]])

这里嵌套列表被解释为一个包含列表的元组,甚至可以是:

In [124]: arr[[0, 1], [1, 0]]
Out[124]: 
array([[ 4,  5,  6,  7],
       [12, 13, 14, 15]])
In [126]: arr[np.array([[0, 1], [1, 0]])].shape
Out[126]: (2, 2, 3, 4)

虽然出现了同样的警告,但遗留代码为什么选择采用元组解释并不十分明显,我没有看到相关文档。


“必须使用numpy源代码编写 - 这是已编译的。因此,我无法轻易地研究它。” - 当然可以 - 我在我的问题下的评论中链接了相关函数 ;) - Eric
1
我已经用“readily”来限定了它。这个函数并不像一些numpy C那样难以理解,但仍然没有Python函数易于测试。 - hpaulj
好的。我正在尝试在Python函数中公开此功能,位于numpy/numpy#8276,因为解析这些奇怪的边角情况对于任何类型的__getitem__转发都是至关重要的。 - Eric
请纠正我在同一页上的答案,如果有错误的话。 - VimNing

0

所以这是我的结论:

  1. [1,2] 显然是一个一维列表。在这种情况下,会触发高级索引。因此,a[[1,2]] 的结果与 a[[1,2],] 相同。
  2. [1, np.array(2)] 被视为二维列表,即使 np.array(2) 是零维的。因此,a[[1, np.array(2)]] 的结果与 a[tuple([1, np.array(2)])] 相同,因此是 a[1, 2],其结果为 0.0

1
我认为我们不需要解释为什么 x[[1,2,slice(None)]] 被视为 x[tuple([1,2,slice(None)])]。我们只需要意识到这个问题,并在可能的情况下避免它。 - hpaulj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接