【问题标题】:Iterate an iterator by chunks (of n) in Python?在Python中按(n)块迭代迭代器?
【发布时间】:2014-07-19 06:43:31
【问题描述】:

你能想出一个很好的方法(也许使用 itertools)来将迭代器分成给定大小的块吗?

因此 l=[1,2,3,4,5,6,7]chunks(l,3) 成为迭代器 [1,2,3], [4,5,6], [7]

我可以想到一个小程序来做到这一点,但可能不是使用 itertools 的好方法。

【问题讨论】:

  • @kindall:由于对最后一个块的处理,这很接近,但不一样。
  • 这有点不同,因为那个问题是关于列表的,而这个问题是更通用的迭代器。虽然答案似乎最终是一样的。
  • @recursive:是的,在完整阅读了链接的线程之后,我发现我的答案中的所有内容都已经出现在另一个线程的某个地方。
  • VTR,因为one of the linked questions 专门针对列表,而不是一般的可迭代对象。

标签: python iterator


【解决方案1】:

itertools 文档的 recipes 中的 grouper() 配方接近您想要的:

def grouper(n, iterable, fillvalue=None):
    "grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    return izip_longest(fillvalue=fillvalue, *args)

不过,它会用填充值填充最后一个块。

一种不太通用的解决方案,它只适用于序列,但可以根据需要处理最后一个块

[my_list[i:i + chunk_size] for i in range(0, len(my_list), chunk_size)]

最后,一个适用于通用迭代器并按需要运行的解决方案是

def grouper(n, iterable):
    it = iter(iterable)
    while True:
        chunk = tuple(itertools.islice(it, n))
        if not chunk:
            return
        yield chunk

【讨论】:

  • @barraponto:不,这是不可接受的,因为你会陷入无限循环。
  • 我很惊讶这是一个如此高票数的答案。该配方适用于小型n,但对于大型团体来说,效率非常低。例如,我的 n 是 200,000。创建一个包含 200K 项的临时列表……并不理想。
  • @JonathanEunice:几乎在所有情况下,这都是人们想要的(这就是它包含在 Python 文档中的原因)。针对特定特殊情况进行优化超出了此问题的范围,即使您在评论中包含了您的信息,我也无法确定最适合您的方法。如果您想将适合内存的数字列表分块,最好使用 NumPy 的 .resize() 消息。如果你想对一个通用迭代器进行分块,第二种方法已经很不错了——它会创建大小为 200K 的临时元组,但这没什么大不了的。
  • @SvenMarnach 我们必须不同意。我相信人们想要方便,而不是无偿的开销。他们得到了开销,因为文档提供了不必要的臃肿答案。对于大数据,临时元组/列表/等。 200K 或 1M 的项目使程序消耗千兆字节的多余内存并需要更长的时间来运行。如果不需要,为什么要这样做?在 200K 时,额外的临时存储使整个程序的运行时间比删除它时长 3.5 倍。就这一点变化。所以这是一个很大的问题。 NumPy 不起作用,因为迭代器是数据库游标,而不是数字列表。
  • @SvenMarnach 我发现我的问题是由于在 Python 2 中使用了zip,它将所有数据加载到内存中,而不是itertools.izip。你可以删除之前的cmets,我也删除这个。
【解决方案2】:

“简单胜于复杂”- 一个几行长的简单生成器就可以完成这项工作。只需将它放在一些实用程序模块中即可:

def grouper (iterable, n):
    iterable = iter(iterable)
    count = 0
    group = []
    while True:
        try:
            group.append(next(iterable))
            count += 1
            if count % n == 0:
                yield group
                group = []
        except StopIteration:
            yield group
            break

【讨论】:

    【解决方案3】:

    这是一个返回惰性块的方法;如果需要列表,请使用 map(list, chunks(...))

    from itertools import islice, chain
    from collections import deque
    
    def chunks(items, n):
        items = iter(items)
        for first in items:
            chunk = chain((first,), islice(items, n-1))
            yield chunk
            deque(chunk, 0)
    
    if __name__ == "__main__":
        for chunk in map(list, chunks(range(10), 3)):
            print chunk
    
        for i, chunk in enumerate(chunks(range(10), 3)):
            if i % 2 == 1:
                print "chunk #%d: %s" % (i, list(chunk))
            else:
                print "skipping #%d" % i
    

    【讨论】:

    • 请评论一下这是如何工作的。
    • 一个警告:这个生成器产生的迭代器只在请求下一个迭代器之前保持有效。当使用例如list(chunks(range(10), 3)),所有的可迭代对象都已经被消费了。
    【解决方案4】:

    我忘记了我是从哪里找到这个灵感的。我对其进行了一些修改,以便在 Windows 注册表中使用 MSI GUID:

    def nslice(s, n, truncate=False, reverse=False):
        """Splits s into n-sized chunks, optionally reversing the chunks."""
        assert n > 0
        while len(s) >= n:
            if reverse: yield s[:n][::-1]
            else: yield s[:n]
            s = s[n:]
        if len(s) and not truncate:
            yield s
    

    reverse 不适用于您的问题,但这是我在此功能中广泛使用的东西。

    >>> [i for i in nslice([1,2,3,4,5,6,7], 3)]
    [[1, 2, 3], [4, 5, 6], [7]]
    >>> [i for i in nslice([1,2,3,4,5,6,7], 3, truncate=True)]
    [[1, 2, 3], [4, 5, 6]]
    >>> [i for i in nslice([1,2,3,4,5,6,7], 3, truncate=True, reverse=True)]
    [[3, 2, 1], [6, 5, 4]]
    

    【讨论】:

    • 这个答案与我开始的答案很接近,但不完全是:stackoverflow.com/a/434349/246801
    • 这仅适用于序列,不适用于一般的可迭代对象。
    • @SvenMarnach:嗨,Sven,是的,谢谢,你说的完全正确。我看到了 OP 的示例,它使用了一个列表(序列)并掩盖了问题的措辞,假设它们的意思是序列。不过,感谢您指出这一点。当我看到你的评论时,我并没有立即理解其中的区别,但后来查了一下。 :)
    【解决方案5】:

    给你。

    def chunksiter(l, chunks):
        i,j,n = 0,0,0
        rl = []
        while n < len(l)/chunks:        
            rl.append(l[i:j+chunks])        
            i+=chunks
            j+=j+chunks        
            n+=1
        return iter(rl)
    
    
    def chunksiter2(l, chunks):
        i,j,n = 0,0,0
        while n < len(l)/chunks:        
            yield l[i:j+chunks]
            i+=chunks
            j+=j+chunks        
            n+=1
    

    示例:

    for l in chunksiter([1,2,3,4,5,6,7,8],3):
        print(l)
    
    [1, 2, 3]
    [4, 5, 6]
    [7, 8]
    
    for l in chunksiter2([1,2,3,4,5,6,7,8],3):
        print(l)
    
    [1, 2, 3]
    [4, 5, 6]
    [7, 8]
    
    
    for l in chunksiter2([1,2,3,4,5,6,7,8],5):
        print(l)
    
    [1, 2, 3, 4, 5]
    [6, 7, 8]
    

    【讨论】:

    • 这仅适用于序列,不适用于一般的可迭代对象。
    【解决方案6】:

    一个简洁的实现是:

    chunker = lambda iterable, n: (ifilterfalse(lambda x: x == (), chunk) for chunk in (izip_longest(*[iter(iterable)]*n, fillvalue=())))
    

    这是有效的,因为[iter(iterable)]*n 是一个包含相同迭代器 n 次的列表;压缩从列表中的每个迭代器中获取一个项目,这是相同的迭代器,结果每个 zip 元素包含一组 n 项目。

    需要izip_longest 来完全消耗底层的可迭代对象,而不是在到达第一个用尽的迭代器时停止迭代,这会切断iterable 的任何剩余部分。这导致需要过滤掉填充值。因此,一个稍微更健壮的实现将是:

    def chunker(iterable, n):
        class Filler(object): pass
        return (ifilterfalse(lambda x: x is Filler, chunk) for chunk in (izip_longest(*[iter(iterable)]*n, fillvalue=Filler)))
    

    这保证了填充值永远不会是底层迭代中的项目。使用上面的定义:

    iterable = range(1,11)
    
    map(tuple,chunker(iterable, 3))
    [(1, 2, 3), (4, 5, 6), (7, 8, 9), (10,)]
    
    map(tuple,chunker(iterable, 2))
    [(1, 2), (3, 4), (5, 6), (7, 8), (9, 10)]
    
    map(tuple,chunker(iterable, 4))
    [(1, 2, 3, 4), (5, 6, 7, 8), (9, 10)]
    

    此实现几乎可以满足您的要求,但存在问题:

    def chunks(it, step):
      start = 0
      while True:
        end = start+step
        yield islice(it, start, end)
        start = end
    

    (不同之处在于,因为islice 不会在超过it 结束的调用上引发 StopIteration 或其他任何事情,这将永远产生;还有一个稍微棘手的问题,islice 结果必须是在迭代此生成器之前消耗)。

    功能性地生成移动窗口:

    izip(count(0, step), count(step, step))
    

    这样就变成了:

    (it[start:end] for (start,end) in izip(count(0, step), count(step, step)))
    

    但是,这仍然会创建一个无限迭代器。所以,你需要暂时(或者其他可能更好的方法)来限制它:

    chunk = lambda it, step: takewhile((lambda x: len(x) > 0), (it[start:end] for (start,end) in izip(count(0, step), count(step, step))))
    
    g = chunk(range(1,11), 3)
    
    tuple(g)
    ([1, 2, 3], [4, 5, 6], [7, 8, 9], [10])
    

    【讨论】:

    • 1.第一个代码 sn-p 包含行 start = end,它似乎没有做任何事情,因为循环的下一次迭代将从 start = 0 开始。此外,循环是无限的——它是while True,没有任何break。 2、第二个代码sn-p中的len是什么? 3. 所有其他实现仅适用于序列,不适用于通用迭代器。 4. 检查x is () 依赖于CPython 的实现细节。作为优化,空元组只创建一次,以后再使用。但是语言规范并不能保证这一点,所以你应该使用x == ()
    • 5. count()takewhile() 的组合使用 range() 更容易实现。
    • @SvenMarnach:我已经编辑了代码和文本以回应您的一些观点。急需打样。
    • 那很快。 :) 我仍然对第一个代码 sn-p 有问题:它仅在产生的切片被消耗时才有效。如果用户不立即消费它们,可能会发生奇怪的事情。这就是 Peter Otten 使用 deque(chunk, 0) 来使用它们的原因,但该解决方案也存在问题 - 请参阅我对他的回答的评论。
    • 我喜欢chunker()的最后一个版本。作为旁注,创建唯一标记的好方法是sentinel = object() - 它保证与任何其他对象不同。
    【解决方案7】:

    虽然 OP 要求函数以列表或元组的形式返回块,但如果您需要返回迭代器,则可以修改 Sven Marnach's 解决方案:

    def grouper_it(n, iterable):
        it = iter(iterable)
        while True:
            chunk_it = itertools.islice(it, n)
            try:
                first_el = next(chunk_it)
            except StopIteration:
                return
            yield itertools.chain((first_el,), chunk_it)
    

    一些基准测试:http://pastebin.com/YkKFvm8b

    仅当您的函数遍历每个块中的元素时,它才会稍微更有效。

    【讨论】:

    • 在文档中找到了答案(这是上面公认的、投票率最高的答案)大量效率低下后,我今天几乎完全得出了这个设计。当您一次对数十万或数百万个对象进行分组时 - 这是您最需要分割的时候 - 它必须非常有效。这是正确的答案。
    • 这是最好的解决方案。
    • 如果调用者没有耗尽chunk_it(例如通过提前中断内部循环),这会不会出现错误的行为?
    • @TavianBarnes 好点,如果第一组没有用尽,第二组将从第一组离开的地方开始。但是,如果您希望两者同时循环,则可以将其视为一项功能。功能强大,但要小心处理。
    • @TavianBarnes:在这种情况下,可以通过制作一个廉价的迭代器消费者来使其行为正确(如果你在循环外创建它,CPython 中最快的是consume = collections.deque(maxlen=0).extend),然后在之后添加consume(chunk_it) yield 行;如果调用者使用了yielded chain,它什么也不做,如果他们没有,它会尽可能有效地代表他们使用它。如果你需要它来将调用者提供的迭代器推进到块的末尾,如果外部循环提前中断,则将其放在 tryfinally 中。
    【解决方案8】:

    我今天正在做一些事情,并想出了一个我认为很简单的解决方案。它类似于jsbueno's 的答案,但我相信当iterable 的长度可以被n 整除时,他会产生空的groups。当iterable 用尽时,我的回答会做一个简单的检查。

    def chunk(iterable, chunk_size):
        """Generates lists of `chunk_size` elements from `iterable`.
        
        
        >>> list(chunk((2, 3, 5, 7), 3))
        [[2, 3, 5], [7]]
        >>> list(chunk((2, 3, 5, 7), 2))
        [[2, 3], [5, 7]]
        """
        iterable = iter(iterable)
        while True:
            chunk = []
            try:
                for _ in range(chunk_size):
                    chunk.append(next(iterable))
                yield chunk
            except StopIteration:
                if chunk:
                    yield chunk
                break
    

    【讨论】:

    • 对于 Python3,您需要将 iterable.next() 更改为 next(iterable)
    【解决方案9】:

    这适用于任何可迭代对象。它返回生成器的生成器(以获得完全的灵活性)。我现在意识到它与@reclosedevs 解决方案基本相同,但没有绒毛。不需要try...except,因为StopIteration 向上传播,这就是我们想要的。

    当可迭代对象为空时,需要调用 next(iterable) 来引发 StopIteration,因为如果您允许,islice 将永远生成空生成器。

    这更好,因为它只有两行长,但易于理解。

    def grouper(iterable, n):
        while True:
            yield itertools.chain((next(iterable),), itertools.islice(iterable, n-1))
    

    注意next(iterable) 被放入一个元组中。否则,如果 next(iterable) 本身是可迭代的,那么 itertools.chain 会将其展平。感谢 Jeremy Brown 指出这个问题。

    【讨论】:

    • 虽然这可能会回答包括部分解释和描述在内的问题,但可能有助于理解您的方法并启发我们了解您的答案为何脱颖而出
    • iterable.next() 需要由一个交互器包含或产生,链才能正常工作 - 例如。 yield itertools.chain([iterable.next()], itertools.islice(iterable, n-1))
    • next(iterable),不是iterable.next()
    • 在 while 循环前加上 iterable = iter(iterable) 行可能是有意义的,以便首先将您的 iterable 转换为 iteratorIterables do not have a __next__ method.
    • 自 PEP479 以来不推荐在生成器函数中提高 StopIteration。所以我更喜欢@reclesedevs 解决方案的显式返回语句。
    猜你喜欢
    • 2012-02-17
    • 1970-01-01
    • 2016-06-21
    • 1970-01-01
    • 2016-07-24
    • 1970-01-01
    • 2019-06-14
    • 1970-01-01
    相关资源
    最近更新 更多