【问题标题】:Python: iterate over a sublistPython:遍历子列表
【发布时间】:2012-07-08 04:01:35
【问题描述】:

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

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

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

# 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)

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

【问题讨论】:

  • 我可能是错的,但我怀疑如果itertools 找不到更有效的方法(通常,如在 islice 中),可能没有办法。
  • 请注意len(items) 是多余的; items[1:] 足以切片列表。
  • 生成器有自己的开销,除非你有一个包含数千个元素的列表,否则创建一个新列表意味着效率更高。

标签: python python-3.x generator slice


【解决方案1】:

使用itertools.islice:

import itertools

l = range(20)

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

10
11
12
13
14

来自文档:

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

【讨论】:

  • 请注意,islice 的 Python 内实现并不比问题中的示例更有效。
  • @kojiro,不确定你的意思。 islice 对象在 cpython 中是 implemented in c
  • @kojiro 这不是真的。我执行了一个快速 timeit 测试,表明使用生成器表达式确实比重新创建列表更快。如果你评论第二个陈述,我真的不明白你的意思。 isclice 正是 OP 所寻找的。获得有限生成器而不是新列表的快速而干净的方法。正如OP已经指出的那样,问题中显示的方式与良好的编码风格相去甚远。我认为它只是被理解为 OP 想要实现的一个例子。
  • @kojiro:第三次timeit测试显示:问题中描述的方法更糟糕:list slice: 1.1535269123993572s; islice:0.87495659566315s;范围生成器:2.8627238423657673s;引用一百万次执行的时间,从 10 到 15(不包括),如我的帖子所示。
  • @kojiro 我只是测量了迭代 10,11,12,13,14 所需的时间,如上所示。迭代器因此被替换为列表切片([10:15]); islice 和生成器表达式。
【解决方案2】:

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

  1. 使用常规切片(复制除了最长输入之外的所有输入的成本通常没有意义,而且代码要简单得多)。 如果输入可能不是可切片的序列类型,请将其转换为 1,然后切片,例如allbutone = list(someiterable)[1:]。这比任何其他方法都更简单,并且在大多数情况下通常更快。
  2. 如果常规切片不可行(不保证输入是序列并且在切片之前转换为序列可能会导致内存问题,或者它很大并且切片覆盖了大部分,例如跳过10M 元素list 的前 1000 个和最后 1000 个元素,因此内存可能是一个问题),itertools.islice 通常是正确的解决方案,因为它足够简单,而且性能成本通常不重要.
  3. 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效率低下),并且在“跳过很多,保留一些”的情况下会做得更糟(相对于急切的切片)。

【讨论】:

  • 旁注:对于跳过一个元素的特定情况,在 Python 3 上,_, *allbutone = iterable 是对 allbutone = list(iterable)[1:] 的改进,当您无法确定 iterable 是否是一个序列时(或者如果list 以外的序列是可能的,并且结果必须是list)。但它不能扩展到n 跳过;即使对于常量 k 跳过(例如 5 次跳过),它也有点难看:_, _, _, _, _, *allbutfive = someiterable。但我认为值得指出,因为它允许在开始结束将其余部分转换为list作为一个步骤,这可能很有用。
【解决方案3】:

试试 itertools.islice:

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

iterrange = itertools.islice(items, 1, None)

【讨论】:

  • 您不需要len(items),只需提供None
  • 您也不需要创建生成器 - 只需 iterrange = itertools.islice(items, 1, None) 就足够了
  • @JonClements 好电话。我试图匹配原件,但显然没有像我那样思考。谢谢。
猜你喜欢
  • 1970-01-01
  • 2013-02-18
  • 1970-01-01
  • 2018-03-08
  • 2015-10-07
  • 2013-07-13
  • 1970-01-01
相关资源
最近更新 更多