在Python中对切片进行高效迭代

时间:2013-08-04 23:35:43

标签: python performance iteration slice

Python中切片操作的迭代效率如何?如果使用切片不可避免地复制副本,还有其他选择吗?

我知道列表上的切片操作是O(k),其中k是切片的大小。

x[5 : 5+k]  # O(k) copy operation

然而,当迭代列表的一部分时,我发现最干净(和大多数Pythonic?)的方式(不必求助于索引)是这样做的:

for elem in x[5 : 5+k]:
  print elem

但是我的直觉是,这仍然会导致子列表的昂贵副本,而不是简单地遍历现有列表。

6 个答案:

答案 0 :(得分:6)

使用:

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部分的副本。因此,即使x中的对象很大,x[5: 5+k]也会创建一个包含k个元素的新列表,这些元素引用与x中相同的相同的对象。因此,您只需要额外的内存来创建一个列表,其中包含对先前存在的对象的k个引用。这可能不会成为任何内存问题的根源。

答案 1 :(得分:5)

您可以使用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的性能取决于切片的起点,并且可迭代的正常切片的大小在几乎所有情况下都会很快,应该是首选。以下是一些时间比较:

对于巨大列表 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

结论:

转到正常切片。

答案 2 :(得分:4)

只需遍历所需的索引,就不需要为此创建新的切片:

for i in xrange(5, 5+k):
    print x[i]

当然:它看起来是单声道的,但它比创建新切片更有效率,因为没有浪费额外的内存。另一种方法是使用迭代器,如@ AshwiniChaudhary的回答所示。

答案 3 :(得分:2)

您已经在切片上进行了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上,以防任何更高版本添加特定于列表的优化。

答案 4 :(得分:0)

我认为更好的方法是使用类似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等效物做了大约10个小时)我告诉的那个大k,因为它需要从一次5到10000000000005! 每次使用'xrange()'的'range()'或甚至切片本身(如上所述)都会使程序执行20000000000000次迭代,这可能会导致更长的执行时间。 (因为我学习使用生成器方法将返回一个可迭代的对象,需要先完成运行生成器,并且需要两倍的时间来完成工作;一个用于生成器本身,另一个用于'for'循环)

已编辑:

在python 3中,生成器方法/对象不需要先运行来为for循环创建可迭代对象

答案 5 :(得分:0)

接受的答案不能提供有效的解决方案。它按n的顺序排列,其中n是切片的起点。如果n大,那将是一个问题。 后续结论(“转到普通切片” )也不理想,因为它以k的顺序使用额外的空间进行复制。

Python为称为生成器表达式的切片问题提供了一种非常优雅而有效的解决方案,它可以尽可能最佳地发挥作用:O(1)空间和O(k)运行时间:

(l[i] for i in range(n,n+k))

它类似于列表推导,只是它懒惰地起作用,并且您可以将其与其他迭代器工具(如itertools模块或过滤器)结合使用。请注意将表达式括起来的圆括号。