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
的早期前身。尽管他不是 numeric
或 numpy
的主要贡献者,但 GvR 与 numeric
开发人员协调,以使多维数组索引更容易的方式扩展 Python 的切片语法。后来,出现了numeric
的替代方案,称为numarray
;并于 2006 年创建了numpy
,融合了两者的最佳特性。
这些库很强大,但它们需要大量的 c 扩展等等。将它们放入基本的 Python 发行版中会使它变得庞大。尽管 GvR 确实稍微增强了切片语法,但向普通列表添加花哨的索引会极大地改变它们的 API——而且有点多余。鉴于已经可以使用外部库进行精美的索引,因此收益不值得付出代价。
老实说,此叙述的部分内容是推测性的。1 我真的不了解开发人员!但这是我会做出的相同决定。其实……
确实应该是这样的。
虽然花哨的索引非常强大,但我很高兴即使在今天它还不是普通 Python 的一部分,因为这意味着您在处理普通列表时不必费力思考。对于许多您不需要它的任务,它所施加的认知负荷是巨大的。
请记住,我说的是施加在 readers 和 maintainers 上的负载。你可能是一个能在头脑中做 5-d 张量积的天才,但其他人必须阅读你的代码。在numpy
中保持精美的索引意味着人们不会使用它,除非他们确实需要它,这使得代码总体上更具可读性和可维护性。
3。为什么numpy的花式索引在python2上这么慢?是因为我在这个版本中没有对 numpy 的原生 BLAS 支持吗?
可能。这绝对取决于环境。我在我的机器上没有看到相同的差异。
1.叙述中不那么推测的部分来自brief history 在科学与工程计算特刊(2011 年第 13 期)中讲述的内容。
【讨论】:
虽然其他答案同样出色,但我对小小的历史课很感兴趣。感谢您提供详细、深思熟虑的回复。 BLAS 仅适用于线性代数【参考方案2】:哪个
__xx__
方法覆盖/定义了numpy 来处理花哨的索引?
__getitem__
用于检索,__setitem__
用于分配。删除是__delitem__
,除了 NumPy 数组不支持删除。
(不过,它们都是用 C 编写的,所以他们在 C 级别实现的是 mp_subscript
和 mp_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
中生成view
。 view
和 copy
之间的区别是理解基本索引和高级索引之间区别的一部分。
所以alist[:][:]
和arr[:,]
是不同的,但它们是制作某种列表和数组副本的基本方法。既不计算任何东西,也不遍历元素。所以时间比较并不能告诉我们太多。
【讨论】:
以上是关于numpy 的花式索引是如何实现的?的主要内容,如果未能解决你的问题,请参考以下文章