numpy 的花式索引是如何实现的?

Posted

技术标签:

【中文标题】numpy 的花式索引是如何实现的?【英文标题】:How is numpy's fancy indexing implemented? 【发布时间】:2017-11-18 08:04:06 【问题描述】:

我正在对 2D 列表和 numpy 数组进行一些实验。由此,我提出了 3 个问题,我很想知道答案。

首先,我初始化了一个 2D python 列表。

>>> my_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

然后我尝试用元组索引列表。

>>> my_list[:,]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: list indices must be integers, not tuple

由于解释器给我的是TypeError而不是SyntaxError,我推测实际上可以做到这一点,但python本身并不支持它。

然后我尝试将列表转换为 numpy 数组并做同样的事情。

>>> np.array(my_list)[:,]
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

当然,没问题。我的理解是__xx__() 方法之一已被覆盖并在numpy 包中实现。

Numpy 的索引也支持列表:

>>> np.array(my_list)[:,[0, 1]]
array([[1, 2],
       [4, 5],
       [7, 8]])

这引发了几个问题:

    哪个__xx__ 方法覆盖/定义了numpy 来处理花哨的索引? 为什么 python 列表本身不支持花哨的索引?

(额外问题:为什么我的时间显示 python2 中的切片比 python3 慢?)

【问题讨论】:

您正在寻找__getitem__。至于这些性能差异,如果不查看您为 python 2 和 3 构建的 numpy 的确切细节,至少无法确定。 @juanpa.arrivillaga 有什么办法可以查到吗? ***.com/questions/37184618/… @juanpa.arrivillaga 感谢您的链接。我已经更新了我的帖子。奇怪的是,这两个版本似乎都支持 BLAS,但是我的 python2 版本比我的 python3 旧。我可能应该升级并再试一次。 我认为时间安排与BLAS 或其他库无关。 list_2[:,] 不是花哨的索引;它是基本的数组索引,并产生view。不涉及计算。 list_1[:] 只是一个列表副本,添加另一个 [:] 只是再次复制。这是一个浅拷贝,而不是深拷贝。 【参考方案1】:

你有三个问题:

1。哪个__xx__ 方法覆盖/定义了numpy 来处理花哨的索引?

索引运算符[] 可以使用__getitem____setitem____delitem__ 覆盖。编写一个提供一些自省的简单子类会很有趣:

>>> class VerboseList(list):
...     def __getitem__(self, key):
...         print(key)
...         return super().__getitem__(key)
...

让我们先做一个空的:

>>> l = VerboseList()

现在用一些值填充它。请注意,我们还没有覆盖 __setitem__,所以还没有发生任何有趣的事情:

>>> l[:] = range(10)

现在让我们得到一个项目。在索引0 将是0

>>> l[0]
0
0

如果我们尝试使用元组,我们会得到一个错误,但我们会先看到元组!

>>> l[0, 4]
(0, 4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in __getitem__
TypeError: list indices must be integers or slices, not tuple

我们还可以找出python内部是如何表示切片的:

>>> l[1:3]
slice(1, 3, None)
[1, 2]

你可以用这个对象做更多有趣的事情——试试吧!

2。为什么 python 列表本身不支持花哨的索引?

这很难回答。一种思考方式是历史性的:因为numpy 开发人员首先想到了它。

你们这些年轻人。小时候……

在 1991 年首次公开发布时,Python 没有 numpy 库,要制作多维列表,您必须嵌套列表结构。我假设早期的开发人员——尤其是 Guido van Rossum (GvR)——最初认为保持简单是最好的。切片索引已经非常强大了。

然而,不久之后,人们对将 Python 作为一种科学计算语言使用的兴趣与日俱增。在 1995 年到 1997 年间,许多开发人员合作开发了一个名为 numeric 的库,它是 numpy 的早期前身。尽管他不是 numericnumpy 的主要贡献者,但 GvR 与 numeric 开发人员协调,以使多维数组索引更容易的方式扩展 Python 的切片语法。后来,出现了numeric 的替代方案,称为numarray;并于 2006 年创建了numpy,融合了两者的最佳特性。

这些库很强大,但它们需要大量的 c 扩展等等。将它们放入基本的 Python 发行版中会使它变得庞大。尽管 GvR 确实稍微增强了切片语法,但向普通列表添加花哨的索引会极大地改变它们的 API——而且有点多余。鉴于已经可以使用外部库进行精美的索引,因此收益不值得付出代价。

老实说,此叙述的部分内容是推测性的。1 我真的不了解开发人员!但这是我会做出的相同决定。其实……

确实应该是这样的。

虽然花哨的索引非常强大,但我很高兴即使在今天它还不是普通 Python 的一部分,因为这意味着您在处理普通列表时不必费力思考。对于许多您不需要它的任务,它所施加的认知负荷是巨大的。

请记住,我说的是施加在 readersmaintainers 上的负载。你可能是一个能在头脑中做 5-d 张量积的天才,但其他人必须阅读你的代码。在numpy 中保持精美的索引意味着人们不会使用它,除非他们确实需要它,这使得代码总体上更具可读性和可维护性。

3。为什么numpy的花式索引在python2上这么慢?是因为我在这个版本中没有对 numpy 的原生 BLAS 支持吗?

可能。这绝对取决于环境。我在我的机器上没有看到相同的差异。


1.叙述中不那么推测的部分来自brief history 在科学与工程计算特刊(2011 年第 13 期)中讲述的内容。

【讨论】:

虽然其他答案同样出色,但我对小小的历史课很感兴趣。感谢您提供详细、深思熟虑的回复。 BLAS 仅适用于线性代数【参考方案2】:

哪个__xx__ 方法覆盖/定义了numpy 来处理花哨的索引?

__getitem__ 用于检索,__setitem__ 用于分配。删除是__delitem__,除了 NumPy 数组不支持删除。

(不过,它们都是用 C 编写的,所以他们在 C 级别实现的是 mp_subscriptmp_ass_subscript__getitem____setitem__ 包装器由 PyType_Ready 提供。__delitem__ 也是虽然不支持删除,因为 __setitem____delitem__ 在 C 级别都映射到 mp_ass_subscript。)

为什么 python 列表本身不支持花哨的索引?

Python 列表基本上是一维结构,而 NumPy 数组是任意维的。多维索引只对多维数据结构有意义。

您可以将列表作为元素的列表,例如[[1, 2], [3, 4]],但列表不知道也不关心其元素的结构。使列表支持l[:, 2] 索引将要求列表以列表未设计的方式了解多维结构。它还会增加很多复杂性、很多错误处理和很多额外的设计决策——l[:, :] 的副本应该有多深?如果结构参差不齐或嵌套不一致会怎样?多维索引是否应该递归到非列表元素中? del l[1:3, 1:3] 会做什么?

我见过 NumPy 索引实现,它比列表的整个实现还要长。 Here's part of it. 当 NumPy 数组满足您需要的所有真正引人注目的用例时,不值得这样做。

为什么 numpy 的花式索引在 python2 上这么慢?是不是因为我在这个版本中没有对 numpy 的原生 BLAS 支持?

NumPy 索引不是 BLAS 操作,所以不是这样。我can'treproduce 如此巨大的时间差异,我看到的差异看起来像是小的 Python 3 优化,可能更有效地分配元组或切片。您看到的可能是由于 NumPy 版本差异造成的。

【讨论】:

在旁注中,ideone 允许您使用 numpy 让我感到惊喜。 @Coldspeed:是的,当我第一次尝试时,他们已经安装了它,这让我感到惊讶,但它非常好。显然他们现在甚至有 SciPy!我认为上次我检查时他们没有。 想一想,我想我在 Ideone 上使用 SciPy 已经有一段时间了,从没想过有什么奇怪的地方。 @senderle:看起来是这样。谷歌也出现了更多用途。我会编辑帖子。【参考方案3】:

my_list[:,]被解释器翻译成

my_list.__getitem__((slice(None, None, None),))

这就像用*args 调用一个函数,但它负责将: 表示法转换为slice 对象。如果没有,,它只会通过slice。使用 , 它传递一个元组。

列表__getitem__ 不接受元组,如错误所示。数组__getitem__ 可以。我相信为numpy(或其前身)添加了传递元组和创建切片对象的能力。元组符号从未添加到列表__getitem__。 (有一个operator.itemgetter 类允许某种形式的高级索引,但在内部它只是一个 Python 代码迭代器。)

使用数组,您可以直接使用元组表示法:

In [490]: np.arange(6).reshape((2,3))[:,[0,1]]
Out[490]: 
array([[0, 1],
       [3, 4]])
In [491]: np.arange(6).reshape((2,3))[(slice(None),[0,1])]
Out[491]: 
array([[0, 1],
       [3, 4]])
In [492]: np.arange(6).reshape((2,3)).__getitem__((slice(None),[0,1]))
Out[492]: 
array([[0, 1],
       [3, 4]])

查看numpy/lib/index_tricks.py 文件,了解您可以使用__getitem__ 做哪些有趣的事情。您可以查看文件

np.source(np.lib.index_tricks)

嵌套列表是列表的列表:

在嵌套列表中,子列表独立于包含列表。容器只有指向内存中其他地方的对象的指针:

In [494]: my_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
In [495]: my_list
Out[495]: [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
In [496]: len(my_list)
Out[496]: 3
In [497]: my_list[1]
Out[497]: [4, 5, 6]
In [498]: type(my_list[1])
Out[498]: list
In [499]: my_list[1]='astring'
In [500]: my_list
Out[500]: [[1, 2, 3], 'astring', [7, 8, 9]]

这里我改了my_list的第2项;它不再是一个列表,而是一个字符串。

如果我将[:] 应用于列表,我只会得到一个浅拷贝:

In [501]: xlist = my_list[:]
In [502]: xlist[1] = 43
In [503]: my_list           # didn't change my_list
Out[503]: [[1, 2, 3], 'astring', [7, 8, 9]]
In [504]: xlist
Out[504]: [[1, 2, 3], 43, [7, 8, 9]]

但更改xlist 中的列表元素确实会更改my_list 中的相应子列表:

In [505]: xlist[0][1]=43
In [506]: my_list
Out[506]: [[1, 43, 3], 'astring', [7, 8, 9]]

对我来说,这通过 n 维索引(如为 numpy 数组实现的那样)显示对嵌套列表没有意义。嵌套列表仅在其内容允许的范围内是多维的;它们没有任何结构或句法上的多维性。

时间安排

在一个列表中使用两个[:] 不会进行深层复制或向下嵌套。它只是重复浅拷贝步骤:

In [507]: ylist=my_list[:][:]
In [508]: ylist[0][1]='boo'
In [509]: xlist
Out[509]: [[1, 'boo', 3], 43, [7, 8, 9]]

arr[:,] 只是在arr 中生成viewviewcopy 之间的区别是理解基本索引和高级索引之间区别的一部分。

所以alist[:][:]arr[:,] 是不同的,但它们是制作某种列表和数组副本的基本方法。既不计算任何东西,也不遍历元素。所以时间比较并不能告诉我们太多。

【讨论】:

以上是关于numpy 的花式索引是如何实现的?的主要内容,如果未能解决你的问题,请参考以下文章

numpy 切片和索引

花式索引的 Numpy 循环广播

各种 numpy 花式索引方法的性能,也与 numba

Numpy 花式索引和赋值

Numpy 花式索引

快速(呃)numpy花式索引和减少?