【问题标题】:Iterate an iterator by chunks (of n) in Python?在Python中按块(n)迭代迭代器?
【发布时间】:2012-02-17 23:24:48
【问题描述】:

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

虽然 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 中。
【解决方案3】:

这适用于任何可迭代对象。它返回生成器的生成器(以获得完全的灵活性)。我现在意识到它与@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 解决方案的显式返回语句。
【解决方案4】:

我今天正在做一些事情,并想出了一个我认为很简单的解决方案。它类似于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)
【解决方案5】:

这是一个返回惰性块的方法;如果需要列表,请使用 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)),所有的可迭代对象都已经被消费了。
【解决方案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】:

我忘记了我是从哪里找到这个灵感的。我对其进行了一些修改,以便在 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 的示例,它使用了一个列表(序列)并掩盖了问题的措辞,假设它们的意思是序列。不过,感谢您指出这一点。当我看到你的评论时,我并没有立即理解其中的区别,但后来查了一下。 :)
【解决方案8】:

给你。

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]

【讨论】:

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

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

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

【讨论】:

    猜你喜欢
    • 2014-07-19
    • 1970-01-01
    • 2016-06-21
    • 1970-01-01
    • 2016-07-24
    • 1970-01-01
    • 2019-06-14
    • 1970-01-01
    相关资源
    最近更新 更多