我正在对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__
这提出了几个问题:
(奖金问题:为什么我的时间表明在python2中切片比python3慢?)
答案 0 :(得分:22)
你有三个问题:
__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]
使用此对象可以做更多有趣的事情 - 试一试!
这很难回答。考虑它的一种方式是历史:因为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 我真的不了解开发者!但这是我做出的同样决定。事实上......
虽然花哨的索引非常强大,但我很高兴它甚至不是今天的vanilla Python的一部分,因为这意味着你在使用普通列表时不必非常努力。对于许多任务,你不需要它,它所带来的认知负荷是很重要的。
请记住,我在谈论对读者和维护者施加的负担。你可能是一个天才的天才,可以在你的头脑中做5-d张量产品,但其他人必须阅读你的代码。在numpy
中保持花哨的索引意味着人们不会使用它,除非他们诚实地需要它,这使得代码更易于阅读和维护。
可能。这绝对是环境依赖的;我在机器上看不出相同的区别。
1。叙述部分不是推测性的,而是来自科学与工程计算特刊(2011年第13卷)中的brief history。
答案 1 :(得分:3)
哪个
__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'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
arr
。 view
和copy
之间的差异是理解基本索引和高级索引之间差异的一部分。
所以alist[:][:]
和arr[:,]
是不同的,但是制作某种列表和数组副本的基本方法。既不计算任何东西,也不迭代元素。所以时间比较并没有告诉我们多少。