在我开始之前,需要明确的是,在切片方法之间选择的正确顺序通常是:
- 使用常规切片(复制除了最长输入之外的所有输入的成本通常没有意义,而且代码要简单得多)。如果输入可能不是可切片的序列类型,请将其转换为 1,然后切片,例如
allbutone = list(someiterable)[1:]
. 这比任何其他方法都更简单,并且在大多数情况下通常更快。
- 如果常规切片不可行(不保证输入是序列并且在切片之前转换为序列可能会导致内存问题,或者它很大并且切片覆盖了大部分,例如跳过前 1000 个和最后 1000 个元素一个 10M 的元素
list
,所以内存可能是一个问题),itertools.islice
通常是正确的解决方案,因为它很简单,而且性能成本通常不重要。
- 当且仅当
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 构建上)说明了这一点(设置中的定义只是一种以最低开销方法运行迭代的方法,因此我们将我们不感兴趣的东西的影响降到最低) :range
ipython3
slurp
>>> 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 效率低下),并且在“跳过很多,保留一些”的情况下,它会做得更糟(相对于急切的切片)。