如何实现numpy的花哨索引?

时间:2017-06-15 18:03:18

标签: python arrays numpy indexing

我正在对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被覆盖并在>>> np.array(my_list)[:,[0, 1]] array([[1, 2], [4, 5], [7, 8]]) 包中实现。

Numpy的索引也支持列表:

__xx__

这提出了几个问题:

  1. 哪个{{1}}方法有numpy覆盖/定义来处理花哨的索引?
  2. 为什么不让python列表本身支持花哨的索引?
  3. (奖金问题:为什么我的时间表明在python2中切片比python3慢?)

3 个答案:

答案 0 :(得分:22)

你有三个问题:

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 我真的不了解开发者!但这是我做出的同样决定。事实上......

确实应该那样。

虽然花哨的索引非常强大,但我很高兴它甚至不是今天的vanilla Python的一部分,因为这意味着你在使用普通列表时不必非常努力。对于许多任务,你不需要它,它所带来的认知负荷是很重要的。

请记住,我在谈论对读者维护者施加的负担。你可能是一个天才的天才,可以在你的头脑中做5-d张量产品,但其他人必须阅读你的代码。在numpy中保持花哨的索引意味着人们不会使用它,除非他们诚实地需要它,这使得代码更易于阅读和维护。

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

可能。这绝对是环境依赖的;我在机器上看不出相同的区别。

1。叙述部分不是推测性的,而是来自科学与工程计算特刊(2011年第13卷)中的brief history

答案 1 :(得分:3)

  

哪个__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't reproduce如此戏剧性的时序差异,我看到的差异看起来像是次要的Python 3优化,可能稍微更有效地分配元组或切片。您所看到的可能是由于NumPy版本的差异。

答案 2 :(得分: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[:,]只需view arrviewcopy之间的差异是理解基本索引和高级索引之间差异的一部分。

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