【问题标题】:how to split an iterable in constant-size chunks如何在恒定大小的块中拆分可迭代
【发布时间】:2012-01-07 14:10:33
【问题描述】:

可能重复:
How do you split a list into evenly sized chunks in 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 本身时才会发生上述行为]

【问题讨论】:

  • 好问题,写得很好,但它已经存在并且会解决你的问题。
  • IMO 这不是真正的重复。另一个问题侧重于列表而不是迭代器,并且大多数答案都需要 len() ,这对于迭代器来说是不可取的。但是呃,这里目前接受的答案也需要len(),所以...
  • 这显然不是重复的。另一个问答仅适用于列表,这个问题是关于泛化到所有可迭代的,这正是我来这里时想到的问题。
  • @JoshSmeaton @casperOne 这不是重复的,接受的答案不正确。链接的重复问题是针对列表的,这是针对可迭代的。 list 提供 len() 方法,但 iterable 不提供 len() 方法,如果不使用 len() 答案会有所不同 这是正确答案:batch = (tuple(filterfalse(lambda x: x is None, group)) for group in zip_longest(fillvalue=None, *[iter(iterable)] * n))
  • @TrideepRath 是的,我已经投票决定重新开放。

标签: python algorithm generator chunking


【解决方案1】:

奇怪,在 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]

【讨论】:

  • 很好的答案,因为它不需要导入任何内容并且易于阅读。
【解决方案2】:

FWIW,recipes in the itertools module 提供了这个例子:

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

【讨论】:

  • 这不是我所需要的,因为它用一组 None 填充最后一个元素。即,None 是我在函数中实际使用的数据中的有效值,所以我需要的是不填充最后一个条目的数据。
  • @mathieu 将izip_longest 替换为izip,这不会填充最后的条目,而是会在某些元素开始用完时切断条目。
  • 在python 3中应该是zip_longest/zip
  • @GoogieK for x, y in enumerate(grouper(3, xrange(10))): print(x,y) 确实不填充值,它只是完全丢弃不完整的段。
  • 作为一个单行,如果不完整则删除最后一个元素:list(zip(*[iter(iterable)] * n))。这一定是我见过的最简洁的 Python 代码。
【解决方案3】:

这可能更有效(更快)

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]

它避免构建新列表。

【讨论】:

  • 记录一下,这是我找到的最快的解决方案:mine = 4.5s, yours=0.43s, Donkopotamus = 14.8s
  • 您的批次实际上接受了一个列表(使用 len()),而不是可迭代的(没有 len())
  • 这更快,因为它不是解决问题的方法。 Raymond Hettinger 的石斑鱼配方 - 目前低于此 - 是您正在寻找的通用解决方案,它不需要输入对象具有 len 方法。
  • 为什么使用 min()?没有min() 代码是完全正确的!
  • Iterables 没有len()sequenceslen()
【解决方案4】:

正如其他人所指出的,您提供的代码完全符合您的要求。对于使用itertools.islice 的另一种方法,您可以看到以下配方的example

from itertools import islice, chain

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

【讨论】:

  • @abhilash No ... 此代码使用对next() 的调用来导致StopIteration 一旦sourceiter 用尽,从而终止迭代器。如果不调用 next,它将继续无限期地返回空迭代器。
  • 我必须将batchiter.next() 替换为next(batchiter) 才能使上述代码在Python 3 中工作。
  • 指出链接文章中的一条评论:“您应该添加一个警告,即必须先用完一批,然后才能继续下一个。”此输出应使用以下内容:map(list, batch(xrange(10), 3))。 Doing:list(batch(xrange(10), 3) 会产生意想不到的结果。
  • 不适用于 py3。 .next()必须改成next(..)list(batch(range(0,10),3))会抛出RuntimeError: generator raised StopIteration
  • @mathieu:将while 循环包裹在try:/except StopIteration: return 以解决后一个问题。
【解决方案5】:

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

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()]

【讨论】:

    【解决方案6】:

    这适用于任何可迭代对象。

    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 值,它将不起作用。

    【讨论】:

      【解决方案7】:

      这是使用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]]
      

      【讨论】:

        【解决方案8】:

        这是一个非常短的代码 sn-p 我知道它不使用 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)))
        

        【讨论】:

          【解决方案9】:

          More-itertools 包含两个功能,可以满足您的需求:

          【讨论】:

          • 这确实是最合适的答案(尽管它需要再安装一个包),而且还有ichunked 可以产生可迭代对象。
          【解决方案10】:
          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]]
          

          【讨论】:

          • 迄今为止的最佳答案,适用于所有数据结构
          【解决方案11】:

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

          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]]
          

          【讨论】:

            【解决方案12】:

            您可能需要的相关功能:

            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 数据帧 (df.iloc[batch(100,0)]) 或 numpy 数组 (array[batch(100,0)])。

            【讨论】:

              【解决方案13】:
              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,)]
              

              【讨论】:

                【解决方案14】:

                如果您正在使用未定义 len 函数的迭代器并感到筋疲力尽,则适用于 Python 3.8 的解决方案:

                from itertools import islice
                
                def batcher(iterable, batch_size):
                    iterator = iter(iterable)
                    while batch := list(islice(iterator, 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]
                
                

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

                【讨论】:

                • 在当前版本中,batcher 接受一个迭代器,而不是一个可迭代对象。例如,它会导致带有列表的无限循环。在开始while 循环之前应该有一行iterator = iter(iterable)
                • from itertools import islice 只是为了完整。 =)
                【解决方案15】:

                我用

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

                【讨论】:

                  【解决方案16】:

                  继续取(最多)n 个元素,直到用完为止。

                  def chop(n, iterable):
                      iterator = iter(iterable)
                      while chunk := list(take(n, iterator)):
                          yield chunk
                  
                  
                  def take(n, iterable):
                      iterator = iter(iterable)
                      for i in range(n):
                          try:
                              yield next(iterator)
                          except StopIteration:
                              return
                  

                  【讨论】:

                    【解决方案17】:

                    python 3.8 中没有新功能的可行版本,改编自 @Atra Azami 的回答。

                    import itertools    
                    
                    def batch_generator(iterable, batch_size=1):
                        iterable = iter(iterable)
                    
                        while True:
                            batch = list(itertools.islice(iterable, batch_size))
                            if len(batch) > 0:
                                yield batch
                            else:
                                break
                    
                    for x in batch_generator(range(0, 10), 3):
                        print(x)
                    

                    输出:

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

                    【讨论】:

                      【解决方案18】:

                      通过利用 islice 和 iter(callable) 行为尽可能多地迁移到 CPython:

                      from itertools import islice
                      
                      def chunked(generator, size):
                          """Read parts of the generator, pause each time after a chunk"""
                          # islice returns results until 'size',
                          # make_chunk gets repeatedly called by iter(callable).
                          gen = iter(generator)
                          make_chunk = lambda: list(islice(gen, size))
                          return iter(make_chunk, [])
                      

                      受 more-itertools 的启发,并简化为该代码的本质。

                      【讨论】:

                        【解决方案19】:

                        此代码具有以下特点:

                        • 可以将列表或生成器(无 len())作为输入
                        • 不需要导入其他包
                        • 最后一批没有添加填充
                        def batch_generator(items, batch_size):
                            itemid=0 # Keeps track of current position in items generator/list
                            batch = [] # Empty batch
                            for item in items: 
                              batch.append(item) # Append items to batch
                              if len(batch)==batch_size:
                                yield batch
                                itemid += batch_size # Increment the position in items
                                batch = []
                            yield batch # yield last bit
                        

                        【讨论】:

                          【解决方案20】:

                          我喜欢这个,

                          def batch(x, bs):
                              return [x[i:i+bs] for i in range(0, len(x), bs)]
                          

                          这会返回一个大小为bs 的批次列表,当然,您可以使用生成器表达式(i for i in iterable) 使其成为生成器。

                          【讨论】:

                          • 这回答了重复的问题,但不是当前的问题
                          猜你喜欢
                          • 2019-06-19
                          • 1970-01-01
                          • 2015-08-13
                          • 2012-10-20
                          • 1970-01-01
                          • 1970-01-01
                          • 2018-03-07
                          • 2015-04-27
                          • 1970-01-01
                          相关资源
                          最近更新 更多