在numpy中,使用空元组和省略号索引数组有什么区别?

14 投票
3 回答
3417 浏览
提问于 2025-04-17 14:42

我偶然发现,numpy中的数组可以用一个空的元组来索引:

In [62]: a = arange(5)

In [63]: a[()]
Out[63]: array([0, 1, 2, 3, 4])

我在numpy的wiki上找到了一些关于零维数组的文档:

(Sasha) 首先,无论你选择用 x[...] 还是 x[()],它们应该是一样的,因为 ... 只是一个语法糖,意思是“根据需要使用多少个 :”,在零维的情况下,这就变成了 ... = (:,)*0 = ()。其次,零维数组和numpy标量类型在numpy中是可以互换的,但numpy标量可以在一些python的结构中使用,而ndarray则不能。

所以,对于0维数组来说,a[()]a[...] 应该是等价的。那么对于更高维的数组呢?它们似乎也是:

In [65]: a = arange(25).reshape(5, 5)

In [66]: a[()] is a[...]
Out[66]: False

In [67]: (a[()] == a[...]).all()
Out[67]: True

In [68]: a = arange(3**7).reshape((3,)*7)

In [69]: (a[()] == a[...]).all()
Out[69]: True

但是,这并不是语法糖。对于高维数组来说,甚至对于0维数组来说也是如此:

In [76]: a[()] is a
Out[76]: False

In [77]: a[...] is a
Out[77]: True

In [79]: b = array(0)

In [80]: b[()] is b
Out[80]: False

In [81]: b[...] is b
Out[81]: True

还有一种情况是用空列表进行索引,这完全是另一回事,但看起来和用空的ndarray索引是等价的:

In [78]: a[[]]
Out[78]: array([], shape=(0, 3, 3, 3, 3, 3, 3), dtype=int64)

In [86]: a[arange(0)]
Out[86]: array([], shape=(0, 3, 3, 3, 3, 3, 3), dtype=int64)

In [82]: b[[]]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)

IndexError: 0-d arrays can't be indexed.

所以,看起来()...是相似的,但并不完全相同,而用[]进行索引则完全是另一种意思。a[]b[]会导致SyntaxError错误。关于用列表进行索引的文档可以在索引数组中找到,还有一段简短的说明关于用元组进行索引的内容,可以在同一文档的末尾找到。

这就留下了一个问题:

a[()]a[...]之间的区别是设计使然吗?那么这个设计是什么呢?

(这个问题让人想起了:空的 `()` 在Matlab矩阵中有什么作用?)

编辑:

事实上,甚至标量也可以用空元组进行索引:

In [36]: numpy.int64(10)[()]
Out[36]: 10

3 个回答

5

根据官方的Numpy文档,这两者的区别很明显:

一个空的(元组)索引是对一个零维数组的完整标量索引。x[()] 如果 x 是零维的,返回一个标量值;如果不是,则返回一个视图。 另一方面,x[...] 总是返回一个视图。

当出现省略号(...)但没有大小(也就是说,替代了零:)时,结果仍然总是一个数组。如果没有高级索引,则返回一个视图;否则返回一个副本。

>>> import numpy as np
>>> # ---------------------------------- #
>>> # when `x` is at least 1 dimensional #
>>> # ---------------------------------- #
>>> x = np.linspace(0, 10, 100)
>>> x.shape
(100,)
>>> x.ndim
1
>>> a = x[()]
>>> b = x[...]
>>> id(x), id(a), id(b)
(4559933568, 4561560080, 4585410192)
>>> id(x.base), id(a.base), id(b.base)
(4560914432, 4560914432, 4560914432)
>>> # ---------------------------- #
>>> # when `z` is zero dimensional #
>>> # ---------------------------- #
>>> z = np.array(3.14)
>>> z.shape
()
>>> z.ndim
0
>>> a = z[()]
>>> b = z[...]
>>> type(a), type(b)
(<class 'numpy.float64'>, <class 'numpy.ndarray'>)
>>> id(z), id(a), id(b)
(4585422896, 4586829384, 4561560080)
>>> id(z.base), id(a.base), id(b.base)
(4557260904, 4557260904, 4585422896)
>>> b.base is z
True
5

在你给出的例子中,空元组和省略号看起来结果差不多,但它们其实是用来做不同事情的。举个例子,当你在索引一个数组时,A[i, j, k] == A[(i, j, k)],而特别地,A[...] == A[(Ellipsis,)]。这里的元组只是用来装索引元素的。这在你需要把索引当作变量来处理时特别有用,比如你可以这样做:

index = (0,) * A.ndim
A[index]

要注意的是,因为元组是用来装索引元素的,所以它不能和其他索引一起用,比如 A[(), 0] == A[[], 0]A[(), 0] != A[..., 0]

因为一个数组 A 可以用比 A.ndim 少的索引来索引,所以用空元组来索引是这种行为的自然延伸,在某些情况下这会很有用,比如上面的代码片段在 A.ndim == 0 时是可以工作的。

简单来说,元组是用来装索引元素的,可以是空的,而省略号则是可能的索引元素之一。

8

A[...] 的处理是一个特殊的情况,经过优化,总是返回 A 本身

if (op == Py_Ellipsis) {
    Py_INCREF(self);
    return (PyObject *)self;
}

其他一些看起来应该是等价的情况,比如 A[:]A[(Ellipsis,)]A[()]A[(slice(None),) * A.ndim],实际上会返回 A 的一个“视图”,这个视图的 base 还是 A

>>> A[()] is A
False
>>> A[()].base is A
True

这看起来是一种不必要且过早的优化,因为 A[(Ellipsis,)]A[()] 总是会给出相同的结果(即对 A 的完整视图)。从这个链接来看,最开始这样做是因为用 ... 来索引 0维数组时不太正常(在这个更新之前,它会把元素当作标量返回),后来为了保持一致性扩展到了所有数组;自那以后,0维数组的索引问题已经修复,所以这个优化其实不再需要,但它还是以某种方式保留了下来(可能还有一些代码依赖于 A[...] is A 这个判断为真)。

撰写回答