Python中切片操作的迭代效率如何?如果切片不可避免地需要复制,还有其他选择吗?
我知道对列表的切片操作是 O(k),其中 k 是切片的大小。
x[5 : 5+k] # O(k) copy operation
但是,当迭代列表的一部分时,我发现最干净(也是最 Pythonic?)的方法(无需求助于索引)是:
for elem in x[5 : 5+k]:
print elem
但是我的直觉是,这仍然会导致子列表的昂贵副本,而不是简单地迭代现有列表。
Python中切片操作的迭代效率如何?如果切片不可避免地需要复制,还有其他选择吗?
我知道对列表的切片操作是 O(k),其中 k 是切片的大小。
x[5 : 5+k] # O(k) copy operation
但是,当迭代列表的一部分时,我发现最干净(也是最 Pythonic?)的方法(无需求助于索引)是:
for elem in x[5 : 5+k]:
print elem
但是我的直觉是,这仍然会导致子列表的昂贵副本,而不是简单地迭代现有列表。
利用:
for elem in x[5 : 5+k]:
这是Pythonic!在您分析代码并确定这是一个瓶颈之前,不要更改它——尽管我怀疑您是否会发现这是瓶颈的主要来源。
就速度而言,它可能是您的最佳选择:
In [30]: x = range(100)
In [31]: k = 90
In [32]: %timeit x[5:5+k]
1000000 loops, best of 3: 357 ns per loop
In [35]: %timeit list(IT.islice(x, 5, 5+k))
100000 loops, best of 3: 2.42 us per loop
In [36]: %timeit [x[i] for i in xrange(5, 5+k)]
100000 loops, best of 3: 5.71 us per loop
在记忆力方面,它并没有你想象的那么糟糕。x[5: 5+k]
是部分的浅拷贝x
。因此,即使 inx
中的对象很大,x[5: 5+k]
也会创建一个包含 k 个元素的新列表,这些元素引用与 in 中相同的对象x
。因此,您只需要额外的内存来创建一个包含 k 对预先存在的对象的引用的列表。这可能不会成为任何内存问题的根源。
您可以使用itertools.islice
从列表中获取切片迭代器:
例子:
>>> from itertools import islice
>>> lis = range(20)
>>> for x in islice(lis, 10, None, 1):
... print x
...
10
11
12
13
14
15
16
17
18
19
正如@user2357112 所指出的,性能islice
取决于切片的起点和可迭代的大小,普通切片在几乎所有情况下都会很快,应该是首选。以下是更多时间比较:
对于Huge 列表 islice
,当切片的起点小于列表大小的一半时,其速度略快或等于普通切片,对于更大的索引,普通切片显然是赢家。
>>> def func(lis, n):
it = iter(lis)
for x in islice(it, n, None, 1):pass
...
>>> def func1(lis, n):
#it = iter(lis)
for x in islice(lis, n, None, 1):pass
...
>>> def func2(lis, n):
for x in lis[n:]:pass
...
>>> lis = range(10**6)
>>> n = 100
>>> %timeit func(lis, n)
10 loops, best of 3: 62.1 ms per loop
>>> %timeit func1(lis, n)
1 loops, best of 3: 60.8 ms per loop
>>> %timeit func2(lis, n)
1 loops, best of 3: 82.8 ms per loop
>>> n = 1000
>>> %timeit func(lis, n)
10 loops, best of 3: 64.4 ms per loop
>>> %timeit func1(lis, n)
1 loops, best of 3: 60.3 ms per loop
>>> %timeit func2(lis, n)
1 loops, best of 3: 85.8 ms per loop
>>> n = 10**4
>>> %timeit func(lis, n)
10 loops, best of 3: 61.4 ms per loop
>>> %timeit func1(lis, n)
10 loops, best of 3: 61 ms per loop
>>> %timeit func2(lis, n)
1 loops, best of 3: 80.8 ms per loop
>>> n = (10**6)/2
>>> %timeit func(lis, n)
10 loops, best of 3: 39.2 ms per loop
>>> %timeit func1(lis, n)
10 loops, best of 3: 39.6 ms per loop
>>> %timeit func2(lis, n)
10 loops, best of 3: 41.5 ms per loop
>>> n = (10**6)-1000
>>> %timeit func(lis, n)
100 loops, best of 3: 18.9 ms per loop
>>> %timeit func1(lis, n)
100 loops, best of 3: 18.8 ms per loop
>>> %timeit func2(lis, n)
10000 loops, best of 3: 50.9 us per loop #clear winner for large index
>>> %timeit func1(lis, n)
对于小型列表,普通切片比islice
几乎所有情况都快。
>>> lis = range(1000)
>>> n = 100
>>> %timeit func(lis, n)
10000 loops, best of 3: 60.7 us per loop
>>> %timeit func1(lis, n)
10000 loops, best of 3: 59.6 us per loop
>>> %timeit func2(lis, n)
10000 loops, best of 3: 59.9 us per loop
>>> n = 500
>>> %timeit func(lis, n)
10000 loops, best of 3: 38.4 us per loop
>>> %timeit func1(lis, n)
10000 loops, best of 3: 33.9 us per loop
>>> %timeit func2(lis, n)
10000 loops, best of 3: 26.6 us per loop
>>> n = 900
>>> %timeit func(lis, n)
10000 loops, best of 3: 20.1 us per loop
>>> %timeit func1(lis, n)
10000 loops, best of 3: 17.2 us per loop
>>> %timeit func2(lis, n)
10000 loops, best of 3: 11.3 us per loop
去普通切片。
只需遍历所需的索引,无需为此创建新切片:
for i in xrange(5, 5+k):
print x[i]
诚然:它看起来不像 Python,但它比创建新切片更有效,因为不会浪费额外的内存。另一种方法是使用迭代器,如@AshwiniChaudhary 的回答所示。
您已经在切片上进行了 O(n) 迭代。在大多数情况下,这将比实际创建切片更令人担忧,这完全发生在优化的 C 中。一旦你制作了切片,循环切片的时间是制作切片的两倍,即使你不要对它做任何事情:
>>> timeit.timeit('l[50:100]', 'import collections; l=range(150)')
0.46978958638010226
>>> timeit.timeit('for x in slice: pass',
'import collections; l=range(150); slice=l[50:100]')
1.2332711270150867
您可以尝试使用 迭代索引xrange
,但考虑到检索列表元素所需的时间,它比切片要慢。即使你跳过那部分,它仍然比不上切片:
>>> timeit.timeit('for i in xrange(50, 100): x = l[i]', 'l = range(150)')
4.3081963062022055
>>> timeit.timeit('for i in xrange(50, 100): pass', 'l = range(150)')
1.675838213385532
不要itertools.islice
用于这个!它将从一开始就遍历您的列表,而不是跳到您想要的值__getitem__
。下面是一些时序数据,显示了它的性能如何取决于切片的开始位置:
>>> timeit.timeit('next(itertools.islice(l, 9, None))', 'import itertools; l = r
ange(1000000)')
0.5628290558478852
>>> timeit.timeit('next(itertools.islice(l, 999, None))', 'import itertools; l =
range(1000000)')
6.885294697594759
这里islice
输给了常规切片:
>>> timeit.timeit('for i in itertools.islice(l, 900, None): pass', 'import itert
ools; l = range(1000)')
8.979957560911316
>>> timeit.timeit('for i in l[900:]: pass', 'import itertools; l = range(1000)')
3.0318417204211983
这是在 Python 2.7.5 上,以防任何更高版本添加特定于列表的优化。
我认为更好的方法是使用类似 c 的迭代,如果 'k' 很大(因为一个大的 'k' - 比如 10000000000000 - 甚至可以让你等待大约 10 个小时才能在 pythonic for 循环中得到答案)
这是我试图告诉你的:
i = 5 ## which is the initial value
f = 5 + k ## which will be the final index
while i < f:
print(x[i])
i += 1
我假设这个可以在 5 小时内完成(就像 pythonic 中的 for 循环一样做大约 10 小时),因为它需要从 5 到 10000000000005 一次!每次使用 'xrange()' 的 'range()' 甚至切片本身(如上所述)都会使程序执行 20000000000000 次迭代,我认为这可能会导致更长的执行时间。(据我所知,使用生成器方法将返回一个可迭代对象,该对象需要首先完全运行生成器,并且需要两次才能完成工作;一个用于生成器本身,另一个用于“for”循环)
编辑:
在 python 3 中,生成器方法/对象不需要首先运行来为 for 循环生成可迭代对象
接受的答案没有提供有效的解决方案。它的顺序是n
,其中n
是切片起点。如果n
很大,那就有问题了。后续结论(“Go for normal slices”)也不理想,因为它使用了额外的空间来k
进行复制。
Python 为切片问题提供了一个非常优雅和高效的解决方案,称为生成器表达式,它可以尽可能地优化:O(1) 空间和 O(k) 运行时间:
(l[i] for i in range(n,n+k))
它类似于列表推导,除了它是惰性的,您可以将它与其他迭代器工具(如 itertools 模块或过滤器)结合使用。注意包围表达式的圆括号。
对于遍历子数组(对于创建子数组,请参阅 unutbu 的答案),切片比在最坏情况下的索引()稍快l[1:]
。
10 items
========
Slicing: 2.570001e-06 s
Indexing: 3.269997e-06 s
100 items
=========
Slicing: 6.820001e-06 s
Indexing: 1.220000e-05 s
1000 items
==========
Slicing: 7.647000e-05 s
Indexing: 1.482100e-04 s
10000 items
===========
Slicing: 2.876200e-04 s
Indexing: 5.270000e-04 s
100000 items
============
Slicing: 3.763300e-03 s
Indexing: 7.731050e-03 s
1000000 items
=============
Slicing: 2.963523e-02 s
Indexing: 4.921381e-02 s
基准代码:
def f_slice(l):
for v in l[1:]:
_x = v
def f_index(l):
for i in range(1, len(l)):
_x = l[i]
from time import perf_counter
def func_time(func, l):
start = perf_counter()
func(l)
return perf_counter()-start
def bench(num_item):
l = list(range(num_item))
times = 10
t_index = t_slice = 0
for _ in range(times):
t_slice += func_time(f_slice, l)
t_index += func_time(f_index, l)
print(f"Slicing: {t_slice/times:e} s")
print(f"Indexing: {t_index/times:e} s")
for i in range(1, 7):
s = f"{10**i} items"
print(s)
print('='*len(s))
bench(10**i)
print()