como dividir um iterável em pedaços de tamanho constante

86

Possível duplicata:
como você divide uma lista em partes de tamanhos iguais no Python?

Estou surpreso por não conseguir encontrar uma função "batch" que receba como entrada um iterável e retorne um iterável de iteráveis.

Por exemplo:

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

ou:

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

Agora, eu escrevi o que pensei ser um gerador bem simples:

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

Mas o que precede não me dá o que eu esperava:

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]

Portanto, perdi algo e isso provavelmente mostra minha total falta de compreensão dos geradores Python. Alguém se importaria em me apontar a direção certa?

[Editar: eventualmente percebi que o comportamento acima ocorre apenas quando eu executo isso dentro do ipython, em vez do próprio python]

Mathieu
fonte
Boa pergunta, bem escrita, mas já existe e vai resolver o seu problema.
Josh Smeaton
7
IMO, isso não é realmente uma duplicata. A outra questão se concentra em listas em vez de iteradores, e a maioria dessas respostas requer len (), o que é indesejável para iteradores. Mas eh, a resposta atualmente aceita aqui também requer len (), então ...
dequis
7
Isso claramente não é uma duplicata. O outro Q&A só funciona para listas , e esta pergunta é sobre generalizar para todos os iteráveis, que é exatamente a pergunta que eu tinha em mente quando vim aqui.
Mark E. Haase
1
@JoshSmeaton @casperOne não é uma duplicata e a resposta aceita não é a correta. A pergunta duplicada vinculada é para lista e isso é iterável. list fornece o método len (), mas iterable não fornece um método len () e a resposta seria diferente sem usar len () Esta é a resposta correta: batch = (tuple(filterfalse(lambda x: x is None, group)) for group in zip_longest(fillvalue=None, *[iter(iterable)] * n))
Trideep Rath
@TrideepRath sim, votei para reabrir.
Josh Smeaton

Respostas:

125

Provavelmente é mais eficiente (mais rápido)

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

Exemplo usando lista

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]

Isso evita a construção de novas listas.

Carl F.
fonte
4
Só para constar, esta é a solução mais rápida que encontrei: meu = 4,5 s, seu = 0,43 s, Donkopotamus = 14,8 s
mathieu
77
seu lote na verdade aceita uma lista (com len ()), não iterável (sem len ())
tdihp
31
Isso é mais rápido porque não é uma solução para o problema. A receita de garoupa de Raymond Hettinger - atualmente abaixo desta - é o que você está procurando para uma solução geral que não requer que o objeto de entrada tenha um método len .
Robert E Mealey,
7
Por que você usa min ()? Sem min()código é totalmente correto!
Pavel Patrin 01 de
21
Iterables não têm len(), seqüências têmlen()
Kos
63

FWIW, as receitas no módulo itertools fornecem este exemplo:

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

Funciona assim:

>>> list(grouper(3, range(10)))
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, None, None)]
Raymond Hettinger
fonte
13
Isso não é exatamente o que eu precisava, pois preenche o último elemento com um conjunto de Nenhum. ou seja, Nenhum é um valor válido nos dados que eu realmente uso com minha função, então o que eu preciso é algo que não preencha a última entrada.
mathieu
12
@mathieu Substitua izip_longestpor izip, que não preenche as últimas entradas, mas corta as entradas quando alguns dos elementos começam a se esgotar.
GoogieK
3
Deve ser zip_longest / zip em python 3
Peter Gerdes
5
@GoogieK de for x, y in enumerate(grouper(3, xrange(10))): print(x,y)fato não preenche valores, ele apenas elimina o segmento incompleto por completo.
kadrach de
3
Como um forro que cai o último elemento se incompleto: list(zip(*[iter(iterable)] * n)). Esse deve ser o trecho de código python mais legal que já vi.
Le Frite
31

Como outros notaram, o código que você forneceu faz exatamente o que você deseja. Para outra abordagem usando, itertools.islicevocê pode ver um exemplo da seguinte receita:

from itertools import islice, chain

def batch(iterable, size):
    sourceiter = iter(iterable)
    while True:
        batchiter = islice(sourceiter, size)
        yield chain([batchiter.next()], batchiter)
donkopotamus
fonte
1
@abhilash Não ... este código usa a chamada next()para fazer com que uma StopIterationvez sourceiterse esgote, encerrando assim o iterador. Sem a chamada para nextele, continuaria a retornar iteradores vazios indefinidamente.
donkopotamus de
7
Tive que substituir batchiter.next()por next(batchiter)para fazer o código acima funcionar no Python 3.
Martin Wiebusch
2
apontando um comentário do artigo vinculado: "Você deve adicionar um aviso de que um lote deve ser totalmente consumido antes de prosseguir para o próximo." A saída disso deve ser consumida com algo como:map(list, batch(xrange(10), 3)) . Fazer: list(batch(xrange(10), 3)produzirá resultados inesperados.
Nathan Buesgens,
2
Não funciona em py3. .next()deve ser alterado para next(..), e list(batch(range(0,10),3))lançaRuntimeError: generator raised StopIteration
mathieu
1
@mathieu: Envolva o whileloop try:/ except StopIteration: returnpara corrigir o último problema.
ShadowRanger
13

Eu apenas dei uma resposta. No entanto, agora sinto que a melhor solução pode ser não escrever nenhuma função nova. More-itertools inclui muitas ferramentas adicionais e chunkedestá entre elas.

Yongwei Wu
fonte
Esta é de fato a resposta mais adequada (embora requeira a instalação de mais um pacote), e também há ichunkediteráveis.
viddik13 de
10

Estranho, parece funcionar bem para mim em 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]
John Doe
fonte
Ótima resposta porque não precisa importar nada e é intuitivo de ler.
ojunk
8

Este é um snippet de código muito curto que eu sei que não usa lene funciona em Python 2 e 3 (não é minha criação):

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

Solução para Python 3.8 se você estiver trabalhando com iteráveis ​​que não definem uma lenfunção e ficar exausto:

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

Exemplo de uso:

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]

Claro que também poderia ser implementado sem o operador de morsa.

Atra Azami
fonte
3
Na versão atual, batcheraceita um iterador, não um iterável. Isso resultaria em um loop infinito com uma lista, por exemplo. Provavelmente deve haver uma linha iterator = iter(iterable)antes de iniciar o whileloop.
Daniel Perez,
2

É isso que uso no meu projeto. Ele lida com iteráveis ​​ou listas da maneira mais eficiente possível.

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()]
Josh Smeaton
fonte
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]]
Atila Romero
fonte
Melhor resposta até agora, funciona com todas as estruturas de dados
Clément Prévost
1

Isso funcionaria para qualquer iterável.

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

Funcionaria assim:

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

PS: Não funcionaria se iterável tivesse nenhum valor.

Trideep Rath
fonte
1

Aqui está uma abordagem usando a reducefunção.

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

Ou versão mais legível:

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

Teste:

>>> 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]]
Lycha
fonte
1

Uma versão funcional sem novos recursos em python 3.8, adaptada da resposta de @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)

Resultado:

[0, 1, 2]
[3, 4, 5]
[6, 7, 8]
[9]
Sua Excelência
fonte
0

Você pode apenas agrupar itens iteráveis ​​por seu índice de lote.

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

Geralmente é o caso quando você deseja coletar iteráveis ​​internos, então aqui está uma versão mais avançada.

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

Exemplos:

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]]
dimathe 47
fonte
0

Funcionalidades relacionadas que você pode precisar:

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

Uso:

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

Ele obtém o i'ésimo lote da sequência e pode trabalhar com outras estruturas de dados também, como pandas dataframes ( df.iloc[batch(100,0)]) ou numpy array ( array[batch(100,0)]).

Alvitawa
fonte
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,)]
yacc143
fonte
0

eu uso

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)]
  
gazorpazorp
fonte
0

Continue pegando (no máximo) n elementos até que acabe.

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
W. Zhu
fonte