如何在恒定大小的块中拆分可迭代


85

可能重复:
如何在Python中将列表分成大小均匀的块?

令我惊讶的是,我找不到“批处理”函数,该函数会将可迭代对象作为输入并返回可迭代对象的可迭代对象。

例如:

for i in batch(range(0,10), 1): print i
[0]
[1]
...
[9]

要么:

for i in batch(range(0,10), 3): print i
[0,1,2]
[3,4,5]
[6,7,8]
[9]

现在,我写了我认为很简单的生成器:

def batch(iterable, n = 1):
   current_batch = []
   for item in iterable:
       current_batch.append(item)
       if len(current_batch) == n:
           yield current_batch
           current_batch = []
   if current_batch:
       yield current_batch

但是以上内容并没有给我我所期望的:

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

所以,我错过了一些东西,这可能表明我完全不了解python生成器。有人愿意指出我正确的方向吗?

[编辑:我最终意识到,只有当我在ipython而不是python本身中运行此行为时,才会发生上述行为]


好的问题,写得很好,但已经存在,可以解决您的问题。
Josh Smeaton

7
IMO这实际上不是重复的。另一个问题关注的是列表,而不是迭代器,并且大多数答案都需要len(),这对于迭代器来说是不希望的。但嗯,这里目前接受的答案也需要len(),所以……
dequis

7
这显然不是重复的。另一个Q&A仅适用于list,这个问题是关于所有可迭代项的泛化,这正是我来这里时想到的问题。
Mark E. Haase

1
@JoshSmeaton @casperOne这不是重复的,接受的答案不正确。链接的重复问题是用于列表的,并且是可迭代的。列表提供了len()方法,但可迭代的方法不提供len()方法,如果不使用len(),答案将有所不同。这是正确的答案: batch = (tuple(filterfalse(lambda x: x is None, group)) for group in zip_longest(fillvalue=None, *[iter(iterable)] * n))
Trideep Rath

@TrideepRath是的,我投票决定要重新开放。
乔什·史密顿

Answers:


117

这可能更有效(更快)

def batch(iterable, n=1):
    l = len(iterable)
    for ndx in range(0, l, n):
        yield iterable[ndx:min(ndx + n, l)]

for x in batch(range(0, 10), 3):
    print x

使用清单范例

data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] # list of data 

for x in batch(data, 3):
    print(x)

# Output

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

它避免建立新列表。


4
根据记录,这是我找到的最快的解决方案:我的= 4.5s,您的= 0.43s,Donkopotamus = 14.8s
mathieu

74
您的批次实际上接受一个列表(带有len()),而不是可迭代的(没有len())
tdihp 2014年

28
这更快,因为它不能解决问题。Raymond Hettinger的石斑鱼食谱(目前在此之下)是您在寻找不需要输入对象具有len方法的常规解决方案的方法。
罗伯特·E·梅阿里2014年

7
为什么要使用min()?没有min()代码是完全正确的!
Pavel Patrin

20
可迭代项没有len()序列len()
-Kos

60

FWIW,itertools模块中配方提供了以下示例:

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

它是这样的:

>>> list(grouper(3, range(10)))
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, None, None)]

13
这并不是我真正需要的,因为它用一组None填充了最后一个元素。即,None是我实际在函数中使用的数据中的有效值,因此我需要的是不会填充最后一个条目的东西。
mathieu 2011年

12
@mathieu替换izip_longestizip,它不会填充最后一个条目,而是在某些元素开始用尽时切断条目。
GoogieK

3
应该是zip_longest / zip in python 3
Peter Gerdes

5
@GoogieKfor x, y in enumerate(grouper(3, xrange(10))): print(x,y)确实不填充值,只是完全丢弃了不完整的段。
kadrach

3
作为一个衬里,如果不完整,它会丢弃最后一个元素:list(zip(*[iter(iterable)] * n))。这必须是我见过的最整洁的python代码。
Le Frite

31

正如其他人指出的那样,您所提供的代码完全可以满足您的需求。对于使用的另一种方法,itertools.islice您可以看到以下示例的示例

from itertools import islice, chain

def batch(iterable, size):
    sourceiter = iter(iterable)
    while True:
        batchiter = islice(sourceiter, size)
        yield chain([batchiter.next()], batchiter)

1
@abhilash否...此代码使用该调用next()导致StopIteration一次sourceiter耗尽,从而终止迭代器。如果没有调用next它,它将继续无限期地返回空迭代器。
donkopotamus

7
我不得不更换batchiter.next()next(batchiter)做在Python 3上面的代码工作
马丁Wiebusch

2
指出链接文章中的评论:“您应该添加警告,必须先完全消耗一批,然后才能继续进行下一个。” 该输出应使用类似的东西来消耗map(list, batch(xrange(10), 3))。这样做:list(batch(xrange(10), 3)会产生意想不到的结果。
Nathan Buesgens

2
在py3上不起作用。.next()必须更改为next(..)list(batch(range(0,10),3))引发RuntimeError: generator raised StopIteration
mathieu '19

1
@mathieu:将while循环包装在try:/中except StopIteration: return以解决后一个问题。
ShadowRanger

13

我只是给出一个答案。但是,现在我觉得最好的解决方案可能是不编写任何新函数。More-itertools包括大量其他工具chunked


这确实是最合适的答案(即使它需要安装一个以上的软件包),并且还会ichunked产生可迭代项。
viddik13

10

很奇怪,似乎在Python 2.x中对我来说工作正常

>>> def batch(iterable, n = 1):
...    current_batch = []
...    for item in iterable:
...        current_batch.append(item)
...        if len(current_batch) == n:
...            yield current_batch
...            current_batch = []
...    if current_batch:
...        yield current_batch
...
>>> for x in batch(range(0, 10), 3):
...     print x
...
[0, 1, 2]
[3, 4, 5]
[6, 7, 8]
[9]

很好的答案,因为它不需要导入任何内容并且直观易读。
ojunk '19

7

这是一个非常简短的代码片段,我知道它不会使用,len并且可以在Python 2和3(不是我的创作)下使用:

def chunks(iterable, size):
    from itertools import chain, islice
    iterator = iter(iterable)
    for first in iterator:
        yield list(chain([first], islice(iterator, size - 1)))

4

如果您正在使用未定义len函数的可迭代对象,并且精疲力竭,则适用于Python 3.8解决方案:

def batcher(iterable, batch_size):
    while batch := list(islice(iterable, batch_size)):
        yield batch

用法示例:

def my_gen():
    yield from range(10)
 
for batch in batcher(my_gen(), 3):
    print(batch)

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

当然也可以在没有海象运算符的情况下实现。


1
在当前版本中,batcher接受迭代器,而不是可迭代的。例如,这将导致列表无限循环。iterator = iter(iterable)在开始while循环之前可能应该有一行。
丹尼尔·佩雷斯

2

这就是我在项目中使用的。它尽可能有效地处理可迭代项或列表。

def chunker(iterable, size):
    if not hasattr(iterable, "__len__"):
        # generators don't have len, so fall back to slower
        # method that works with generators
        for chunk in chunker_gen(iterable, size):
            yield chunk
        return

    it = iter(iterable)
    for i in range(0, len(iterable), size):
        yield [k for k in islice(it, size)]


def chunker_gen(generator, size):
    iterator = iter(generator)
    for first in iterator:

        def chunk():
            yield first
            for more in islice(iterator, size - 1):
                yield more

        yield [k for k in chunk()]

2
def batch(iterable, n):
    iterable=iter(iterable)
    while True:
        chunk=[]
        for i in range(n):
            try:
                chunk.append(next(iterable))
            except StopIteration:
                yield chunk
                return
        yield chunk

list(batch(range(10), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

最佳答案到目前为止,作品与每一个数据结构
克莱门特·普雷沃

1

这将适用于任何迭代。

from itertools import zip_longest, filterfalse

def batch_iterable(iterable, batch_size=2): 
    args = [iter(iterable)] * batch_size 
    return (tuple(filterfalse(lambda x: x is None, group)) for group in zip_longest(fillvalue=None, *args))

它将像这样工作:

>>>list(batch_iterable(range(0,5)), 2)
[(0, 1), (2, 3), (4,)]

PS:如果iterable具有None值,则它将不起作用。


1

这是使用reduce函数的一种方法。

Oneliner:

from functools import reduce
reduce(lambda cumulator,item: cumulator[-1].append(item) or cumulator if len(cumulator[-1]) < batch_size else cumulator + [[item]], input_array, [[]])

或更可读的版本:

from functools import reduce
def batch(input_list, batch_size):
  def reducer(cumulator, item):
    if len(cumulator[-1]) < batch_size:
      cumulator[-1].append(item)
      return cumulator
    else:
      cumulator.append([item])
    return cumulator
  return reduce(reducer, input_list, [[]])

测试:

>>> batch([1,2,3,4,5,6,7], 3)
[[1, 2, 3], [4, 5, 6], [7]]
>>> batch(a, 8)
[[1, 2, 3, 4, 5, 6, 7]]
>>> batch([1,2,3,None,4], 3)
[[1, 2, 3], [None, 4]]

0

您可以按批次索引对可迭代项进行分组。

def batch(items: Iterable, batch_size: int) -> Iterable[Iterable]:
    # enumerate items and group them by batch index
    enumerated_item_groups = itertools.groupby(enumerate(items), lambda t: t[0] // batch_size)
    # extract items from enumeration tuples
    item_batches = ((t[1] for t in enumerated_items) for key, enumerated_items in enumerated_item_groups)
    return item_batches

当您想收集内部可迭代对象时,通常是这种情况,因此这里是更高级的版本。

def batch_advanced(items: Iterable, batch_size: int, batches_mapper: Callable[[Iterable], Any] = None) -> Iterable[Iterable]:
    enumerated_item_groups = itertools.groupby(enumerate(items), lambda t: t[0] // batch_size)
    if batches_mapper:
        item_batches = (batches_mapper(t[1] for t in enumerated_items) for key, enumerated_items in enumerated_item_groups)
    else:
        item_batches = ((t[1] for t in enumerated_items) for key, enumerated_items in enumerated_item_groups)
    return item_batches

例子:

print(list(batch_advanced([1, 9, 3, 5, 2, 4, 2], 4, tuple)))
# [(1, 9, 3, 5), (2, 4, 2)]
print(list(batch_advanced([1, 9, 3, 5, 2, 4, 2], 4, list)))
# [[1, 9, 3, 5], [2, 4, 2]]

0

您可能需要的相关功能:

def batch(size, i):
    """ Get the i'th batch of the given size """
    return slice(size* i, size* i + size)

用法:

>>> [1,2,3,4,5,6,7,8,9,10][batch(3, 1)]
>>> [4, 5, 6]

它从序列中获得第i个批处理,并且还可以与其他数据结构一起使用,例如pandas dataframes(df.iloc[batch(100,0)])或numpy array(array[batch(100,0)])。


0
from itertools import *

class SENTINEL: pass

def batch(iterable, n):
    return (tuple(filterfalse(lambda x: x is SENTINEL, group)) for group in zip_longest(fillvalue=SENTINEL, *[iter(iterable)] * n))

print(list(range(10), 3)))
# outputs: [(0, 1, 2), (3, 4, 5), (6, 7, 8), (9,)]
print(list(batch([None]*10, 3)))
# outputs: [(None, None, None), (None, None, None), (None, None, None), (None,)]

0

我用

def batchify(arr, batch_size):
  num_batches = math.ceil(len(arr) / batch_size)
  return [arr[i*batch_size:(i+1)*batch_size] for i in range(num_batches)]
  
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.