19

通常,当您想在 Python 中迭代列表的一部分时,最简单的做法就是对列表进行切片。

# Iterate over everything except the first item in a list
#
items = [1,2,3,4]
iterrange = (x for x in items[1:])

但是切片操作符会创建一个新列表,这在很多情况下甚至是不必要的。理想情况下,我想要某种创建生成器的切片函数,而不是新的列表对象。可以通过创建使用 arange仅返回列表的某些部分的生成器表达式来完成类似的操作:

# Create a generator expression that returns everything except 
# the first item in the list
#
iterrange = (x for x, idx in zip(items, range(0, len(items))) if idx != 0)

但这有点麻烦。我想知道是否有更好,更优雅的方式来做到这一点。那么,对列表进行切片以便创建生成器表达式而不是新的列表对象的最简单方法是什么?

4

3 回答 3

21

使用itertools.islice

import itertools

l = range(20)

for i in itertools.islice(l,10,15):
    print i

10
11
12
13
14

从文档:

创建一个从可迭代对象中返回选定元素的迭代器

于 2012-07-08T13:31:13.950 回答
2

在我开始之前,需要明确的是,在切片方法之间选择的正确顺序通常是:

  1. 使用常规切片(复制除了最长输入之外的所有输入的成本通常没有意义,而且代码要简单得多)。如果输入可能不是可切片的序列类型,请将其转换为 1,然后切片,例如allbutone = list(someiterable)[1:]. 这比任何其他方法都更简单,并且在大多数情况下通常更快。
  2. 如果常规切片不可行(不保证输入是序列并且在切片之前转换为序列可能会导致内存问题,或者它很大并且切片覆盖了大部分,例如跳过前 1000 个和最后 1000 个元素一个 10M 的元素list,所以内存可能是一个问题),itertools.islice通常是正确的解决方案,因为它很简单,而且性能成本通常不重要。
  3. 当且仅当islice的性能慢得令人无法接受(它为生成每个项目增加了一些开销,尽管诚然这是一个很小的数量)并且要跳过的数据量很小,而要包含的数据量很大(例如,OP 跳过单个元素并保留其余元素的场景),继续阅读

如果您发现自己处于第 3 种情况,那么您处于这样一种情况,即islice' 快速绕过初始元素的能力(相对)不足以弥补生成其余元素的增量成本。在这种情况下,您可以通过将问题从选择所有元素after n转变为丢弃所有元素before n来提高性能。

对于这种方法,您手动将输入转换为迭代器,然后显式提取并丢弃n值,然后迭代迭代器中剩余的内容(但没有每个元素的开销islice)。例如,对于 的输入myinput = list(range(1, 10000)),您选择元素 1 到末尾的选项是:

# Approach 1, OP's approach, simple slice:
for x in myinput[1:]:

# Approach 2, Sebastian's approach, using itertools.islice:
for x in islice(myinput, 1, None):

# Approach 3 (my approach)
myiter = iter(myinput)  # Explicitly create iterator from input (looping does this already)
next(myiter, None) # Throw away one element, providing None default to avoid StopIteration error
for x in myiter:  # Iterate unwrapped iterator

如果要丢弃的元素数量较大,最好从文档中借用consume配方itertools

def consume(iterator, n=None):
    "Advance the iterator n-steps ahead. If n is None, consume entirely."
    # Use functions that consume iterators at C speed.
    if n is None:
        # feed the entire iterator into a zero-length deque
        collections.deque(iterator, maxlen=0)
    else:
        # advance to the empty slice starting at position n
        next(islice(iterator, n, n), None)

这使得这些方法可以概括为跳过n元素:

# Approach 1, OP's approach, simple slice:
for x in myinput[n:]:

# Approach 2, Sebastian's approach, using itertools.islice:
for x in islice(myinput, n, None):

# Approach 3 (my approach)
myiter = iter(myinput)  # Explicitly create iterator from input (looping does this already)
consume(myiter, n)      # Throw away n elements
# Or inlined consume as next(islice(myiter, n, n), None)
for x in myiter:        # Iterate unwrapped iterator

在性能方面,对于大多数大型输入来说,这会以有意义的数量获胜(例外: Python 3 上的自身已经针对普通切片进行了优化;在实际对象range上无法击败普通切片)。微基准测试(在 CPython 3.6,64 位 Linux 构建上)说明了这一点(设置中的定义只是一种以最低开销方法运行迭代的方法,因此我们将我们不感兴趣的东西的影响降到最低) :rangeipython3slurp

>>> from itertools import islice
>>> from collections import deque
>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10000))
... slurp(r[1:])
...
65.8 μs ± 109 ns per loop (mean ± std. dev. of 5 runs, 10000 loops each)

>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10000))
... slurp(islice(r, 1, None))
...
70.7 μs ± 104 ns per loop (mean ± std. dev. of 5 runs, 10000 loops each)

>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10000))
... ir = iter(r)
... next(islice(ir, 1, 1), None)  # Inlined consume for simplicity, but with islice wrapping to show generalized usage
... slurp(ir)
...
30.3 μs ± 64.1 ns per loop (mean ± std. dev. of 5 runs, 10000 loops each)

显然,我的解决方案的额外复杂性通常不值得,但对于中等大小的输入(在本例中为 10K 元素),性能优势是显而易见的;islice是表现最差的(少量),普通切片稍微好一点(这强化了我的观点,即当你有一个实际序列时,普通切片几乎总是最好的解决方案),以及“转换为迭代器,丢弃初始,使用休息" 相对而言,方法赢得了巨大的胜利(远低于任何一种解决方案的一半时间)。

这种好处不会出现在微小的输入上,因为加载/调用iter/的固定开销next,尤其是islice,将超过节省的成本:

>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10))
... slurp(r[1:])
...
207 ns ± 1.86 ns per loop (mean ± std. dev. of 5 runs, 1000000 loops each)

>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10))
... slurp(islice(r, 1, None))
...
307 ns ± 1.71 ns per loop (mean ± std. dev. of 5 runs, 1000000 loops each)

>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10))
... ir = iter(r)
... next(islice(ir, 1, 1), None)  # Inlined consume for simplicity, but with islice wrapping to show generalized usage
... slurp(ir)
...
518 ns ± 4.5 ns per loop (mean ± std. dev. of 5 runs, 1000000 loops each)

>>> %%timeit -r5 slurp = deque(maxlen=0).extend; r = list(range(10))
... ir = iter(r)
... next(ir, None)  # To show fixed overhead of islice, use next without it
... slurp(ir)
...
341 ns ± 0.947 ns per loop (mean ± std. dev. of 5 runs, 1000000 loops each)

但正如您所见,即使是 10 个元素,islice-free 方法也不会差多少;通过 100 个元素,islice-free 方法比所有竞争对手都快,并且通过 200 个元素,广义next+islice击败了所有竞争对手(显然它没有击败islice-free,因为 180 ns 的开销islice,但这可以通过推广到跳过来弥补n元素作为一个步骤,而不是需要next重复调​​用以跳过多个元素)。清楚的islice由于包装器所要求的每个元素的开销,很少在“跳过一些,保留很多”的情况下获胜(直到大约 100K 元素,它才明显击败微基准中的急切切片;它的内存效率很高,但 CPU 效率低下),并且在“跳过很多,保留一些”的情况下,它会做得更糟(相对于急切的切片)。

于 2018-11-07T18:13:59.957 回答
0

试试 itertools.islice:

http://docs.python.org/library/itertools.html#itertools.islice

iterrange = itertools.islice(items, 1, None)
于 2012-07-08T13:33:35.017 回答