在我开始之前,需要明确的是,在切片方法之间选择的正确顺序通常是:
-
使用常规切片(复制除了最长输入之外的所有输入的成本通常没有意义,而且代码要简单得多)。 如果输入可能不是可切片的序列类型,请将其转换为 1,然后切片,例如
allbutone = list(someiterable)[1:]。这比任何其他方法都更简单,并且在大多数情况下通常更快。
-
如果常规切片不可行(不保证输入是序列并且在切片之前转换为序列可能会导致内存问题,或者它很大并且切片覆盖了大部分,例如跳过10M 元素
list 的前 1000 个和最后 1000 个元素,因此内存可能是一个问题),itertools.islice 通常是正确的解决方案,因为它足够简单,而且性能成本通常不重要.
1234563要跳过的数据很小,而要包含的数据很大(例如OP的跳过单个元素并保留其余元素的场景),请继续阅读
如果您发现自己遇到了第 3 种情况,那么您就处于 islice 快速绕过初始元素的能力(相对)不足以弥补生产其余部分的增量成本的情况元素。在这种情况下,您可以通过将问题从选择所有元素之后n转变为丢弃所有元素之前 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
如果要丢弃的元素数量较多,可能最好借用the consume recipe from the itertools docs:
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 本身已经针对普通切片进行了优化;在实际的 range 对象上无法击败普通切片)。 ipython3 微基准测试(在 CPython 3.6,64 位 Linux 构建上)说明了这一点(设置中 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 是表现最差的(少量),普通切片稍好一些(这强化了我的观点,即当你有一个实际序列时,普通切片几乎总是最好的解决方案),以及“转换为迭代器,丢弃初始, use rest" 方法相对而言赢得了巨大的胜利(远低于任何一种解决方案的一半时间)。
这种好处不会出现在微小的输入上,因为加载/调用 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效率低下),并且在“跳过很多,保留一些”的情况下会做得更糟(相对于急切的切片)。