如何以块形式迭代列表

How to iterate over a list in chunks

提问人:Ben Blank 提问时间:1/12/2009 最后编辑:Ben Blank 更新时间:10/21/2023 访问量:249513

问:

我有一个 Python 脚本,它以整数列表作为输入,我需要一次处理四个整数。不幸的是,我无法控制输入,否则我会将其作为四元素元组列表传入。目前,我正在以这种方式迭代它:

for i in range(0, len(ints), 4):
    # dummy op for example code
    foo += ints[i] * ints[i + 1] + ints[i + 2] * ints[i + 3]

不过,它看起来很像“C-think”,这让我怀疑有一种更像 python 的方式来处理这种情况。该列表在迭代后将被丢弃,因此无需保留。也许这样的事情会更好?

while ints:
    foo += ints[0] * ints[1] + ints[2] * ints[3]
    ints[0:4] = []

不过,仍然不太“感觉”正确。:-/

更新:随着 Python 1.12 的发布,我更改了公认的答案。对于尚未(或不能)跳转到 1.12 的任何人,我鼓励您查看之前接受的答案或下面任何其他出色的、向后兼容的答案。

相关问题:如何在 Python 中将列表拆分为大小均匀的块?

Python 列表 循环 优化

评论

4赞 Pedro Henriques 1/12/2009
如果列表大小不是 4 的倍数,则代码不起作用。
6赞 Ben Blank 1/12/2009
我正在扩展()列表,使其长度是 4 的倍数,然后才能走到这一步。
6赞 Ben Blank 7/22/2011
@ΤΖΩΤΖΙΟΥ — 这些问题非常相似,但并不完全重复。它是“拆分为任意数量的 N 大小的块”与“拆分为任何大小的 N 块”。:-)
3赞 dbr 6/23/2012
如何在 Python 中将列表拆分为大小均匀的块的可能副本?
1赞 mkrieger1 4/10/2022
这回答了你的问题吗?如何将列表拆分为大小均匀的块?

答:

2赞 Greg Hewgill 1/12/2009 #1

在你的第二种方法中,我会通过这样做进入下一个 4 人组:

ints = ints[4:]

但是,我没有做过任何性能测量,所以我不知道哪一个可能更有效。

话虽如此,我通常会选择第一种方法。它并不漂亮,但这通常是与外界接触的结果。

31赞 Markus Jarderot 1/12/2009 #2
import itertools
def chunks(iterable,size):
    it = iter(iterable)
    chunk = tuple(itertools.islice(it,size))
    while chunk:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

# though this will throw ValueError if the length of ints
# isn't a multiple of four:
for x1,x2,x3,x4 in chunks(ints,4):
    foo += x1 + x2 + x3 + x4

for chunk in chunks(ints,4):
    foo += sum(chunk)

另一种方式:

import itertools
def chunks2(iterable,size,filler=None):
    it = itertools.chain(iterable,itertools.repeat(filler,size-1))
    chunk = tuple(itertools.islice(it,size))
    while len(chunk) == size:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

# x2, x3 and x4 could get the value 0 if the length is not
# a multiple of 4.
for x1,x2,x3,x4 in chunks2(ints,4,0):
    foo += x1 + x2 + x3 + x4

评论

2赞 Sergey Golovchenko 1/12/2009
+1 用于使用生成器,接缝是所有建议解决方案中最“pythonic”的
9赞 zenazn 1/12/2009
对于如此简单的事情来说,它相当长且笨拙,这根本不是 pythonic。我更喜欢 S. Lott 的版本
4赞 Janus Troelsen 11/26/2012
@zenazn:这将适用于生成器实例,切片则不然
0赞 dano 8/20/2014
除了正确使用生成器和其他不可切片的迭代器之外,如果最终块小于 ,则第一个解决方案也不需要“填充”值,这有时是可取的。size
1赞 Cuadue 4/11/2015
发电机也+1。其他解决方案需要调用,因此不适用于其他生成器。len
219赞 S.Lott 1/12/2009 #3
chunk_size = 4
for i in range(0, len(ints), chunk_size):
    chunk = ints[i:i+chunk_size]
    # process chunk of size <= chunk_size

评论

1赞 PlsWork 2/18/2019
如果 len(ints) 不是 chunkSize 的倍数,它的行为如何?
11赞 3/26/2019
@AnnaVopureta最后一批元素将有 1、2 或 3 个元素。请参阅此问题,了解为什么切片索引可能越界chunk
0赞 Lou 12/3/2022
对不依赖 itertools 的解决方案投了赞成票。有一个开箱即用的 Python 解决方案真是太好了。
605赞 nosklo 1/12/2009 #4
def chunker(seq, size):
    return (seq[pos:pos + size] for pos in range(0, len(seq), size))

适用于任何序列:

text = "I am a very, very helpful text"

for group in chunker(text, 7):
   print(repr(group),)
# 'I am a ' 'very, v' 'ery hel' 'pful te' 'xt'

print('|'.join(chunker(text, 10)))
# I am a ver|y, very he|lpful text

animals = ['cat', 'dog', 'rabbit', 'duck', 'bird', 'cow', 'gnu', 'fish']

for group in chunker(animals, 3):
    print(group)
# ['cat', 'dog', 'rabbit']
# ['duck', 'bird', 'cow']
# ['gnu', 'fish']

评论

21赞 jfs 1/12/2009
@Carlos Crasborn 的版本适用于任何可迭代对象(而不仅仅是上述代码中的序列);它简明扼要,可能同样快,甚至更快。尽管对于不熟悉模块的人来说,它可能有点晦涩难懂(不清楚)。itertools
7赞 Dror 2/24/2015
请注意,返回 .将返回值替换为 : 以获取列表。chunkergeneratorreturn [...]
15赞 Alfe 4/15/2016
除了编写函数构建然后返回生成器之外,还可以直接编写生成器,使用 : 。我不确定在内部是否会在任何相关方面处理任何不同,但它可能会更清晰一点。yieldfor pos in xrange(0, len(seq), size): yield seq[pos:pos + size]
5赞 apollov 12/23/2017
请注意,这仅适用于支持按索引访问项目的序列,不适用于泛型迭代器,因为它们可能不支持方法。__getitem__
1赞 nosklo 5/25/2019
@smci上面的函数是一个生成器 - 它返回一个生成器表达式chunker()
4赞 Robert Rossney 1/12/2009 #5

如果列表很大,则执行此操作的最佳方法是使用生成器:

def get_chunk(iterable, chunk_size):
    result = []
    for item in iterable:
        result.append(item)
        if len(result) == chunk_size:
            yield tuple(result)
            result = []
    if len(result) > 0:
        yield tuple(result)

for x in get_chunk([1,2,3,4,5,6,7,8,9,10], 3):
    print x

(1, 2, 3)
(4, 5, 6)
(7, 8, 9)
(10,)

评论

0赞 Robert Rossney 1/12/2009
(我认为 MizardX 的 itertools 建议在功能上等同于此。
1赞 Robert Rossney 1/12/2009
(实际上,经过反思,不,我没有。 itertools.islice 返回一个迭代器,但它不使用现有的迭代器。
0赞 Valentas 10/8/2018
它很好,很简单,但由于某种原因,即使没有转换为元组,也比公认的石斑鱼方法慢 4-7 倍,最高可达 10000。iterable = range(100000000)chunksize
0赞 Valentas 10/8/2018
但是,一般来说,我会推荐这种方法,因为当检查最后一项时,接受的方法可能会非常慢 docs.python.org/3/library/itertools.html#itertools.zip_longest
12赞 Pedro Henriques 1/12/2009 #6
from itertools import izip_longest

def chunker(iterable, chunksize, filler):
    return izip_longest(*[iter(iterable)]*chunksize, fillvalue=filler)

评论

0赞 jfs 1/12/2009
一种可读的方法是 stackoverflow.com/questions/434287/......
0赞 mdmjsh 10/11/2019
请注意,在 python 中,3 被替换为izip_longestzip_longest
437赞 Craz 1/12/2009 #7

修改自 Python 的 itertools 文档的 Recipes 部分:

from itertools import zip_longest

def grouper(iterable, n, fillvalue=None):
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)

grouper('ABCDEFGHIJ', 3, 'x')  # --> 'ABC' 'DEF' 'GHI' 'Jxx'

注意:在 Python 2 上,使用 代替 .izip_longestzip_longest

评论

81赞 Ben Blank 1/13/2009
终于有机会在 python 会话中玩这个。对于那些和我一样困惑的人来说,这是多次将同一个迭代器提供给izip_longest,导致它消耗同一序列的连续值,而不是来自不同序列的条带化值。我喜欢!
8赞 gotgenes 8/27/2009
过滤掉填充值的最佳方法是什么?([item for item in item if item is not fillvalue] for items in grouper(iterable))?
24赞 anatoly techtonik 4/28/2013
我怀疑这个石斑鱼配方对 256k 大小块的性能会很差,因为会提供 256k 参数。izip_longest
17赞 LondonRob 8/14/2015
在一些地方,评论者说:“当我终于弄清楚这是如何工作的......”也许需要一些解释。特别是迭代器列表方面。
13赞 CMCDragonkai 12/11/2018
有没有办法使用它,但又不填满最后一个块?None
11赞 jfs 1/12/2009 #8

由于还没有人提到它,这里有一个解决方案:zip()

>>> def chunker(iterable, chunksize):
...     return zip(*[iter(iterable)]*chunksize)

仅当您的序列长度始终可被块大小整除时,它才有效,或者您不关心尾随块(如果不是)。

例:

>>> s = '1234567890'
>>> chunker(s, 3)
[('1', '2', '3'), ('4', '5', '6'), ('7', '8', '9')]
>>> chunker(s, 4)
[('1', '2', '3', '4'), ('5', '6', '7', '8')]
>>> chunker(s, 5)
[('1', '2', '3', '4', '5'), ('6', '7', '8', '9', '0')]

或者使用 itertools.izip 返回迭代器而不是列表:

>>> from itertools import izip
>>> def chunker(iterable, chunksize):
...     return izip(*[iter(iterable)]*chunksize)

可以使用@ΤΖΩΤΖΙΟΥ的答案来修复填充:

>>> from itertools import chain, izip, repeat
>>> def chunker(iterable, chunksize, fillvalue=None):
...     it   = chain(iterable, repeat(fillvalue, chunksize-1))
...     args = [it] * chunksize
...     return izip(*args)

评论

0赞 xenoid 4/6/2023
这个真是太聪明了......
4赞 catwell 11/29/2011 #9

使用 map() 而不是 zip() 修复了 J.F. Sebastian 回答中的填充问题:

>>> def chunker(iterable, chunksize):
...   return map(None,*[iter(iterable)]*chunksize)

例:

>>> s = '1234567890'
>>> chunker(s, 3)
[('1', '2', '3'), ('4', '5', '6'), ('7', '8', '9'), ('0', None, None)]
>>> chunker(s, 4)
[('1', '2', '3', '4'), ('5', '6', '7', '8'), ('9', '0', None, None)]
>>> chunker(s, 5)
[('1', '2', '3', '4', '5'), ('6', '7', '8', '9', '0')]

评论

2赞 ShadowRanger 10/1/2016
这最好用 (Py2)/ (Py3) 处理;这种用法是双重弃用的,在 Py3 中不可用(你不能作为映射器函数传递,当最短的可迭代对象用尽时,它停止,而不是最长的可迭代对象;它不会填充)。itertools.izip_longestitertools.zip_longestmapNone
18赞 rhettg 5/29/2012 #10

这个问题的理想解决方案适用于迭代器(而不仅仅是序列)。它也应该很快。

这是 itertools 文档提供的解决方案:

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

在我的 mac book air 上使用 ipython,我每个循环得到 47.5 个我们。%timeit

但是,这真的对我不起作用,因为结果被填充为偶数大小的组。没有填充的解决方案稍微复杂一些。最幼稚的解决方案可能是:

def grouper(size, iterable):
    i = iter(iterable)
    while True:
        out = []
        try:
            for _ in range(size):
                out.append(i.next())
        except StopIteration:
            yield out
            break
        
        yield out

简单,但速度很慢:每个循环 693 us

我能想到的用于内部循环的最佳解决方案:islice

def grouper(size, iterable):
    it = iter(iterable)
    while True:
        group = tuple(itertools.islice(it, None, size))
        if not group:
            break
        yield group

使用相同的数据集,我每个循环得到 305 个 us。

由于无法更快地获得纯解决方案,我提供了以下解决方案,并有一个重要的警告:如果您的输入数据中有 的实例,您可能会得到错误的答案。filldata

def grouper(n, iterable, fillvalue=None):
    #"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    # itertools.zip_longest on Python 3
    for x in itertools.izip_longest(*args, fillvalue=fillvalue):
        if x[-1] is fillvalue:
            yield tuple(v for v in x if v is not fillvalue)
        else:
            yield x

我真的不喜欢这个答案,但它要快得多。每个回路 124 us

评论

1赞 ShadowRanger 9/30/2016
您可以通过将配方 #3 移动到 C 层(省略导入; 必须是 Py3 或 ): 。通过使用哨兵可以使您的最终函数变得不那么脆弱:摆脱参数;添加第一行,然后将选中的行更改为 ,将它控制的行更改为 。保证不会将 in 值误认为填充值。itertoolsmapmapimapdef grouper(n, it): return takewhile(bool, map(tuple, starmap(islice, repeat((iter(it), n)))))fillvaluefillvalue = object()ifif i[-1] is fillvalue:yield tuple(v for v in i if v is not fillvalue)iterable
0赞 ShadowRanger 9/30/2016
顺便说一句,对#4竖起大拇指。我正准备发布我对 #3 的优化,作为比迄今为止发布的更好的答案(性能方面),但经过调整以使其可靠、有弹性,#4 的运行速度是优化后的 #3 的两倍多;我没想到一个带有 Python 级别循环(并且没有理论算法差异 AFAICT)的解决方案会获胜。我假设 #3 由于构造/迭代对象的费用而失败(如果相对较大,例如组数很少,则 #3 获胜,但这是针对不常见情况进行优化),但我没想到它会那么极端。islicen
0赞 Kumba 8/15/2017
对于 #4,条件的第一个分支仅在最后一次迭代(最终元组)中采用。与其重新构建最终元组,不如在顶部缓存原始可迭代对象长度的模,并使用它来切掉最后一个元组上不需要的填充:.此外,对于变量,用元组代替列表:.再剃掉几个时钟周期。最后,如果我们忽略 fillvalue 并假设 ,条件可以变成更多的时钟周期。izip_longestyield i[:modulo]argsargs = (iter(iterable),) * nNoneif None in i
1赞 ShadowRanger 11/14/2017
@Kumba:您的第一个建议假定输入的长度已知。如果它是迭代器/生成器,而不是具有已知长度的集合,则无需缓存任何内容。无论如何,没有真正的理由使用这样的优化;您正在优化不常见情况(最后一个),而常见情况不受影响。yield
1赞 elhefe 11/12/2012 #11

另一个答案,其优点是:

1) 易于理解
2) 适用于任何可迭代对象,而不仅仅是序列(上述一些答案会因文件句柄而窒息) 3) 不会一次
将块全部加载到内存中 4) 不会在内存
中对同一迭代器进行引用的块长列表 5)
列表末尾没有填充值

话虽如此,我没有计时,所以它可能比一些更聪明的方法慢,而且考虑到用例,一些优点可能无关紧要。

def chunkiter(iterable, size):
  def inneriter(first, iterator, size):
    yield first
    for _ in xrange(size - 1): 
      yield iterator.next()
  it = iter(iterable)
  while True:
    yield inneriter(it.next(), it, size)

In [2]: i = chunkiter('abcdefgh', 3)
In [3]: for ii in i:                                                
          for c in ii:
            print c,
          print ''
        ...:     
        a b c 
        d e f 
        g h 

更新:由于内部循环和外部循环从同一迭代器中提取值,因此存在一些缺点:

1)继续在外部循环中无法按预期工作 - 它只是继续到下一项,而不是跳过一个块。但是,这似乎不是问题,因为在外部循环中没有什么可测试的。
2) break 在内部循环中没有按预期工作 - 控制将再次在内部循环中结束,迭代器中的下一个项目。要跳过整个块,要么将内部迭代器(上面的 ii)包装在一个元组中,例如 ,要么设置一个标志并耗尽迭代器。
for c in tuple(ii)

11赞 kriss 12/6/2012 #12

与其他提案类似,但不完全相同,我喜欢这样做,因为它简单易读:

it = iter([1, 2, 3, 4, 5, 6, 7, 8, 9])
for chunk in zip(it, it, it, it):
    print chunk

>>> (1, 2, 3, 4)
>>> (5, 6, 7, 8)

这样你就不会得到最后一个部分块。如果要获取最后一个块,只需使用 from .(9, None, None, None)izip_longestitertools

评论

0赞 Jean-François Fabre 1/1/2019
可以通过以下方式进行改进zip(*([it]*4))
0赞 kriss 1/2/2019
@Jean-弗朗索瓦·法布尔:从可读性的角度来看,我不认为这是一种改进。而且速度也略慢。如果你打高尔夫球,这是一种进步,而我不是。
1赞 Jean-François Fabre 1/2/2019
不,我不是在打高尔夫球,但如果你有 10 个论点怎么办?我在一些官方页面上读到了这个结构,但当然我现在似乎找不到它:)
1赞 kriss 1/2/2019
@Jean-François Fabre:如果我有 10 个参数,或者参数数量可变,这是一个选项,但我宁愿写:zip(*(it,)*10)
0赞 Jean-François Fabre 1/2/2019
右!这就是我读到的。不是我编造的清单:)
4赞 Will 2/21/2013 #13

使用小功能和东西真的没有吸引力;我更喜欢只使用切片:

data = [...]
chunk_size = 10000 # or whatever
chunks = [data[i:i+chunk_size] for i in xrange(0,len(data),chunk_size)]
for chunk in chunks:
    ...

评论

0赞 n611x007 4/24/2014
很好,但对于一个未知的无限流没有好处。您可以使用 或 进行测试。lenitertools.repeatitertools.cycle
1赞 n611x007 4/24/2014
此外,由于使用列表推导式来物理构建列表,而不是使用只关心下一个元素和备用内存的生成器表达式,因此会占用内存[...for...](...for...)
18赞 bcoughlan 8/15/2013 #14

我需要一个也适用于发电机组和发电机的解决方案。我想不出任何非常简短和漂亮的东西,但至少它是相当可读的。

def chunker(seq, size):
    res = []
    for el in seq:
        res.append(el)
        if len(res) == size:
            yield res
            res = []
    if res:
        yield res

列表:

>>> list(chunker([i for i in range(10)], 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

设置:

>>> list(chunker(set([i for i in range(10)]), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

发电机:

>>> list(chunker((i for i in range(10)), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
1赞 Wilfred Hughes 2/20/2014 #15
def group_by(iterable, size):
    """Group an iterable into lists that don't exceed the size given.

    >>> group_by([1,2,3,4,5], 2)
    [[1, 2], [3, 4], [5]]

    """
    sublist = []

    for index, item in enumerate(iterable):
        if index > 0 and index % size == 0:
            yield sublist
            sublist = []

        sublist.append(item)

    if sublist:
        yield sublist

评论

0赞 n611x007 4/24/2014
+1 它省略了填充;你的和 Bcoughlan 的非常相似
6赞 senderle 2/27/2014 #16

另一种方法是使用以下两个参数形式:iter

from itertools import islice

def group(it, size):
    it = iter(it)
    return iter(lambda: tuple(islice(it, size)), ())

这可以很容易地适应使用填充(这类似于 Markus Jarderot 的答案):

from itertools import islice, chain, repeat

def group_pad(it, size, pad=None):
    it = chain(iter(it), repeat(pad))
    return iter(lambda: tuple(islice(it, size)), (pad,) * size)

这些甚至可以组合成可选的填充:

_no_pad = object()
def group(it, size, pad=_no_pad):
    if pad == _no_pad:
        it = iter(it)
        sentinel = ()
    else:
        it = chain(iter(it), repeat(pad))
        sentinel = (pad,) * size
    return iter(lambda: tuple(islice(it, size)), sentinel)

评论

1赞 n611x007 4/24/2014
更可取,因为您可以选择省略填充!
1赞 Suor 6/5/2014 #17

您可以使用funcy库中的分区函数:

from funcy import partition

for a, b, c, d in partition(4, ints):
    foo += a * b * c * d

这些函数还具有迭代器版本和 ,在这种情况下效率会更高。ipartitionichunks

您还可以查看它们的实现情况。

3赞 Tutul 9/1/2014 #18

单行临时解决方案,用于以大小块的形式迭代列表x4 -

for a, b, c, d in zip(x[0::4], x[1::4], x[2::4], x[3::4]):
    ... do something with a, b, c and d ...
3赞 John Mee 10/18/2014 #19

要避免所有转换为列表,并且:import itertools

>>> for k, g in itertools.groupby(xrange(35), lambda x: x/10):
...     list(g)

生产:

... 
0 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
2 [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
3 [30, 31, 32, 33, 34]
>>> 

我检查了一下,它没有转换为列表或使用,所以我(认为)这将延迟每个值的解析,直到它实际使用。可悲的是,(目前)似乎没有一个可用的答案提供这种变化。groupbylen

显然,如果您需要依次处理每个项目,请在 g 上嵌套 a for 循环:

for k,g in itertools.groupby(xrange(35), lambda x: x/10):
    for i in g:
       # do what you need to do with individual items
    # now do what you need to do with the whole group

我对此特别感兴趣的是需要使用生成器来批量向 gmail API 提交最多 1000 个更改:

    messages = a_generator_which_would_not_be_smart_as_a_list
    for idx, batch in groupby(messages, lambda x: x/1000):
        batch_request = BatchHttpRequest()
        for message in batch:
            batch_request.add(self.service.users().messages().modify(userId='me', id=message['id'], body=msg_labels))
        http = httplib2.Http()
        self.credentials.authorize(http)
        batch_request.execute(http=http)

评论

0赞 PaulMcG 10/17/2015
如果您要分块的列表不是升序整数序列,该怎么办?
0赞 John Mee 10/19/2015
@PaulMcGuire参见 groupby;给定一个描述顺序的函数,那么可迭代的元素可以是任何东西,对吧?
2赞 PaulMcG 10/20/2015
是的,我对 groupby 很熟悉。但是,如果消息是字母“ABCDEFG”,那么会给你一个 TypeError(用于尝试将字符串除以 int),而不是 3 个字母的分组。现在,如果你这样做了,你可能会得到一些东西。但你没有在你的帖子中这么说。groupby(messages, lambda x: x/3)groupby(enumerate(messages), lambda x: x[0]/3)
2赞 endolith 11/19/2014 #20

使用 NumPy,这很简单:

ints = array([1, 2, 3, 4, 5, 6, 7, 8])
for int1, int2 in ints.reshape(-1, 2):
    print(int1, int2)

输出:

1 2
3 4
5 6
7 8
1赞 GingerPlusPlus 12/3/2014 #21

关于这里给出的解决方案:J.F. Sebastian

def chunker(iterable, chunksize):
    return zip(*[iter(iterable)]*chunksize)

它很聪明,但有一个缺点 - 总是返回元组。如何获取字符串?
当然你可以写,但临时元组还是被构造了。
''.join(chunker(...))

你可以通过编写 own 来摆脱临时元组,如下所示:zip

class IteratorExhausted(Exception):
    pass

def translate_StopIteration(iterable, to=IteratorExhausted):
    for i in iterable:
        yield i
    raise to # StopIteration would get ignored because this is generator,
             # but custom exception can leave the generator.

def custom_zip(*iterables, reductor=tuple):
    iterators = tuple(map(translate_StopIteration, iterables))
    while True:
        try:
            yield reductor(next(i) for i in iterators)
        except IteratorExhausted: # when any of iterators get exhausted.
            break

然后

def chunker(data, size, reductor=tuple):
    return custom_zip(*[iter(data)]*size, reductor=reductor)

用法示例:

>>> for i in chunker('12345', 2):
...     print(repr(i))
...
('1', '2')
('3', '4')
>>> for i in chunker('12345', 2, ''.join):
...     print(repr(i))
...
'12'
'34'

评论

3赞 Alfe 4/15/2016
不是要你改变答案的批评,而是评论:代码是一种负担。您编写的代码越多,您为隐藏 bug 创造的空间就越大。从这个角度来看,重写而不是使用现有的似乎不是最好的主意。zip
2赞 Kamil Sindi 2/25/2016 #22
def chunker(iterable, n):
    """Yield iterable in chunk sizes.

    >>> chunks = chunker('ABCDEF', n=4)
    >>> chunks.next()
    ['A', 'B', 'C', 'D']
    >>> chunks.next()
    ['E', 'F']
    """
    it = iter(iterable)
    while True:
        chunk = []
        for i in range(n):
            try:
                chunk.append(next(it))
            except StopIteration:
                yield chunk
                raise StopIteration
        yield chunk

if __name__ == '__main__':
    import doctest

    doctest.testmod()
1赞 BallpointBen 6/9/2017 #23

我喜欢这种方法。它感觉简单而不神奇,支持所有可迭代类型,不需要导入。

def chunk_iter(iterable, chunk_size):
it = iter(iterable)
while True:
    chunk = tuple(next(it) for _ in range(chunk_size))
    if not chunk:
        break
    yield chunk
1赞 Andrey Cizov 7/7/2017 #24

这里很蟒蛇(您也可以内联函数的主体)split_groups

import itertools
def split_groups(iter_in, group_size):
    return ((x for _, x in item) for _, item in itertools.groupby(enumerate(iter_in), key=lambda x: x[0] // group_size))

for x, y, z, w in split_groups(range(16), 4):
    foo += x * y + z * w
2赞 frankish 7/20/2017 #25

我从来不想要填充我的块,所以这个要求是必不可少的。我发现在任何可迭代对象上工作的能力也是必需的。鉴于此,我决定扩展公认的答案,https://stackoverflow.com/a/434411/1074659

如果由于需要比较和筛选填充值而不需要填充,则此方法的性能会受到轻微影响。但是,对于较大的块大小,此实用程序的性能非常高。

#!/usr/bin/env python3
from itertools import zip_longest


_UNDEFINED = object()


def chunker(iterable, chunksize, fillvalue=_UNDEFINED):
    """
    Collect data into chunks and optionally pad it.

    Performance worsens as `chunksize` approaches 1.

    Inspired by:
        https://docs.python.org/3/library/itertools.html#itertools-recipes

    """
    args = [iter(iterable)] * chunksize
    chunks = zip_longest(*args, fillvalue=fillvalue)
    yield from (
        filter(lambda val: val is not _UNDEFINED, chunk)
        if chunk[-1] is _UNDEFINED
        else chunk
        for chunk in chunks
    ) if fillvalue is _UNDEFINED else chunks
27赞 MSeifert 9/29/2017 #26

如果您不介意使用外部软件包,则可以使用iteration_utilties 1 中的 iteration_utilities.grouper。它支持所有可迭代对象(而不仅仅是序列):

from iteration_utilities import grouper
seq = list(range(20))
for group in grouper(seq, 4):
    print(group)

打印:

(0, 1, 2, 3)
(4, 5, 6, 7)
(8, 9, 10, 11)
(12, 13, 14, 15)
(16, 17, 18, 19)

如果长度不是组大小的倍数,它还支持填充(不完整的最后一组)或截断(丢弃不完整的最后一组)最后一个组:

from iteration_utilities import grouper
seq = list(range(17))
for group in grouper(seq, 4):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)
# (16,)

for group in grouper(seq, 4, fillvalue=None):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)
# (16, None, None, None)

for group in grouper(seq, 4, truncate=True):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)

基准

我还决定比较一些上述方法的运行时间。它是一个对数-对数图,根据不同大小的列表分组为“10”个元素组。对于定性结果:越低意味着越快:

enter image description here

至少在这个基准测试中,表现最好。紧随其后的是克拉兹的接近。iteration_utilities.grouper

该基准测试是使用 simple_benchmark1 创建的。用于运行此基准测试的代码为:

import iteration_utilities
import itertools
from itertools import zip_longest

def consume_all(it):
    return iteration_utilities.consume(it, None)

import simple_benchmark
b = simple_benchmark.BenchmarkBuilder()

@b.add_function()
def grouper(l, n):
    return consume_all(iteration_utilities.grouper(l, n))

def Craz_inner(iterable, n, fillvalue=None):
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)

@b.add_function()
def Craz(iterable, n, fillvalue=None):
    return consume_all(Craz_inner(iterable, n, fillvalue))

def nosklo_inner(seq, size):
    return (seq[pos:pos + size] for pos in range(0, len(seq), size))

@b.add_function()
def nosklo(seq, size):
    return consume_all(nosklo_inner(seq, size))

def SLott_inner(ints, chunk_size):
    for i in range(0, len(ints), chunk_size):
        yield ints[i:i+chunk_size]

@b.add_function()
def SLott(ints, chunk_size):
    return consume_all(SLott_inner(ints, chunk_size))

def MarkusJarderot1_inner(iterable,size):
    it = iter(iterable)
    chunk = tuple(itertools.islice(it,size))
    while chunk:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

@b.add_function()
def MarkusJarderot1(iterable,size):
    return consume_all(MarkusJarderot1_inner(iterable,size))

def MarkusJarderot2_inner(iterable,size,filler=None):
    it = itertools.chain(iterable,itertools.repeat(filler,size-1))
    chunk = tuple(itertools.islice(it,size))
    while len(chunk) == size:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

@b.add_function()
def MarkusJarderot2(iterable,size):
    return consume_all(MarkusJarderot2_inner(iterable,size))

@b.add_arguments()
def argument_provider():
    for exp in range(2, 20):
        size = 2**exp
        yield size, simple_benchmark.MultiArgument([[0] * size, 10])

r = b.run()

1 免责声明:我是库的作者,.iteration_utilitiessimple_benchmark

评论

0赞 user3507825 11/25/2022
这个模块很棒。让我觉得我作为一个程序员在作弊。感谢您在这里发布。
3赞 Alexey 3/28/2019 #27

除非我遗漏了什么,否则没有提到以下带有生成器表达式的简单解决方案。它假设块的大小和数量都是已知的(通常是这样),并且不需要填充:

def chunks(it, n, m):
    """Make an iterator over m first chunks of size n.
    """
    it = iter(it)
    # Chunks are presented as tuples.
    return (tuple(next(it) for _ in range(n)) for _ in range(m))
36赞 kafran 4/26/2020 #28

从 Python 3.8 开始,您可以使用 walrus 运算符和 .:=itertools.islice

from itertools import islice

list_ = [i for i in range(10, 100)]

def chunker(it, size):
    iterator = iter(it)
    while chunk := list(islice(iterator, size)):
        print(chunk)
In [2]: chunker(list_, 10)                                                         
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
[30, 31, 32, 33, 34, 35, 36, 37, 38, 39]
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
[50, 51, 52, 53, 54, 55, 56, 57, 58, 59]
[60, 61, 62, 63, 64, 65, 66, 67, 68, 69]
[70, 71, 72, 73, 74, 75, 76, 77, 78, 79]
[80, 81, 82, 83, 84, 85, 86, 87, 88, 89]
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]

19赞 teekarna 11/11/2020 #29

more-itertools 包具有分块方法,它正是这样做的:

import more_itertools
for s in more_itertools.chunked(range(9), 4):
    print(s)

指纹

[0, 1, 2, 3]
[4, 5, 6, 7]
[8]

chunked返回列表中的项。如果您更喜欢可迭代对象,请使用 ichunked

12赞 ShadowRanger 4/11/2023 #30

从 Python 3.12 开始,itertools 模块获得了一个批处理函数,该函数专门用于迭代输入可迭代对象的批次,其中最终批次可能不完整(每个批次都是 )。根据文档中给出的示例代码:tuple

>>> for batch in batched('ABCDEFG', 3):
...     print(batch)
...
('A', 'B', 'C')
('D', 'E', 'F')
('G',)

性能说明:

与迄今为止的所有函数一样,批处理的实现位于 C 层,因此它能够进行 Python 级别代码无法匹配的优化,例如itertools

  • 在每次拉取新批次时,它都会主动分配精确正确大小的元素(对于除最后一批之外的所有批次),而不是通过摊销增长逐个元素构建 up,从而导致多次重新分配(就像调用 an 的解决方案所做的那样)tupletupletupleislice
  • 它只需要每批次查找一次底层迭代器的功能,而不需要每批次查找次数(基于方法的方式).__next__nzip_longest((iter(iterable),) * n)
  • 对最终情况的检查是一个简单的 C 级检查(微不足道,无论如何都需要处理可能的异常)NULL
  • 处理最终情况是 C 后跟一个直接(不将副本复制成更小的)到已知的最终大小,因为它正在跟踪它成功拉取了多少元素(没有复杂的“创建哨兵以用作并执行 Python 级别/检查每个批次以查看它是否为空,最后一批需要搜索最后出现的位置, 创建基于解决方案所需的削减)。gotorealloctuplefillvalueifelsefillvaluetuplezip_longest

在所有这些优点之间,它应该大大优于任何 Python 级别的解决方案(甚至是高度优化的解决方案,将大部分或全部每个项目的工作推送到 C 层),无论输入可迭代对象是长还是短,也无论批大小和最终(可能不完整)批次的大小(基于 zip_longest 的解决方案是否使用保证的唯一填充值s 以确保安全对于几乎所有不可用的情况,都是最好的解决方案,但在“大批量很少,最终批次大部分,没有完全填充”的病理情况下,它们可能会受到影响,尤其是在 3.10 之前,当不能用于优化从线性搜索到二进制搜索的切片时,但完全避免了该搜索,因此它根本不会遇到病理病例)。itertools.batchedbisectfillvalueO(n)O(log n)batched

评论

3赞 Ben Blank 4/12/2023
很高兴看到这个功能来到标准库中!一旦 3.12 发布并开始广泛使用,我就必须让这个答案成为公认的答案。🙂
0赞 ShadowRanger 10/20/2023
@BenBlank:它发布了!它的工作原理与广告宣传的一样!很高兴他们终于把它变成了一个内置的,很难在 Python 层使用其他工具针对一般情况进行很好的优化。