come dividere un iterabile in blocchi di dimensioni costanti


87

Possibile duplicato:
come si divide un elenco in blocchi di dimensioni uguali in Python?

Sono sorpreso di non essere riuscito a trovare una funzione "batch" che prenda come input un iterabile e restituisca un iterabile di iterabili.

Per esempio:

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

o:

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

Ora, ho scritto quello che pensavo fosse un generatore piuttosto semplice:

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

Ma quanto sopra non mi dà quello che mi sarei aspettato:

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]

Quindi, mi sono perso qualcosa e questo probabilmente mostra la mia completa mancanza di comprensione dei generatori Python. Qualcuno vorrebbe indicarmi la giusta direzione?

[Modifica: alla fine mi sono reso conto che il comportamento di cui sopra si verifica solo quando lo eseguo in ipython piuttosto che in python stesso]


Bella domanda, ben scritta, ma esiste già e risolverà il tuo problema.
Josh Smeaton

7
IMO questo non è davvero un duplicato. L'altra domanda si concentra sugli elenchi invece che sugli iteratori e la maggior parte di queste risposte richiede len (), che non è desiderabile per gli iteratori. Ma eh, la risposta attualmente accettata qui richiede anche len (), quindi ...
dequis

7
Questo chiaramente non è un duplicato. L'altra domanda e risposta funziona solo per gli elenchi e questa domanda riguarda la generalizzazione a tutti gli iterabili, che è esattamente la domanda che avevo in mente quando sono arrivato qui.
Mark E. Haase

1
@JoshSmeaton @casperOne questo non è un duplicato e la risposta accettata non è corretta. La domanda duplicata collegata è per l'elenco e questo è per iterabile. list fornisce il metodo len () ma iterable non fornisce un metodo len () e la risposta sarebbe diversa senza usare len () Questa è la risposta corretta: batch = (tuple(filterfalse(lambda x: x is None, group)) for group in zip_longest(fillvalue=None, *[iter(iterable)] * n))
Trideep Rath

@TrideepRath sì, ho votato per riaprire.
Josh Smeaton

Risposte:


126

Questo è probabilmente più efficiente (più veloce)

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

Esempio utilizzando list

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]

Evita di costruire nuove liste.


4
Per la cronaca, questa è la soluzione più veloce che ho trovato: mio = 4.5s, tuo = 0.43s, Donkopotamus = 14.8s
mathieu

77
il tuo batch infatti accetta una lista (con len ()), non iterabile (senza len ())
tdihp

31
È più veloce perché non è una soluzione al problema. La ricetta della cernia di Raymond Hettinger - attualmente sotto questa - è ciò che stai cercando per una soluzione generale che non richieda che l'oggetto di input abbia un metodo len .
Robert E Mealey

7
Perché usi min ()? Senza min()codice è completamente corretto!
Pavel Patrin

21
Gli iterabili non hanno len(), le sequenze hannolen()
Kos

64

FWIW, le ricette nel modulo itertools forniscono questo esempio:

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

Funziona così:

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

13
Questo non è esattamente ciò di cui avevo bisogno poiché riempie l'ultimo elemento con un set di Nessuno. cioè Nessuno è un valore valido nei dati che uso effettivamente con la mia funzione, quindi quello che mi serve è qualcosa che non riempia l'ultima voce.
mathieu

12
@mathieu Sostituisci izip_longestcon izip, che non riempirà le ultime voci, ma taglierà invece le voci quando alcuni degli elementi iniziano a esaurirsi.
GoogieK

3
Dovrebbe essere zip_longest / zip in Python 3
Peter Gerdes

5
@GoogieK in for x, y in enumerate(grouper(3, xrange(10))): print(x,y)effetti non riempie i valori, elimina del tutto il segmento incompleto.
kadrach

3
Come uno di linea che scende l'ultimo elemento se incompleti: list(zip(*[iter(iterable)] * n)). Questo deve essere il pezzo più pulito di codice Python che abbia mai visto.
Le Frite

31

Come altri hanno notato, il codice che hai fornito fa esattamente quello che vuoi. Per un altro approccio utilizzando itertools.islicepotresti vedere un esempio della seguente ricetta:

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 No ... questo codice utilizza la chiamata a next()per far sì che una StopIterationvolta sourceitersia esaurita, terminando così l'iteratore. Senza la chiamata a nextesso continuerebbe a restituire iteratori vuoti a tempo indeterminato.
donkopotamus

7
Ho dovuto sostituire batchiter.next()con next(batchiter)per far funzionare il codice sopra in Python 3.
Martin Wiebusch

2
sottolineando un commento dall'articolo collegato: "È necessario aggiungere un avviso che informa che un batch deve essere completamente consumato prima di poter procedere a quello successivo." L'uscita di questo deve essere consumato con qualcosa come: map(list, batch(xrange(10), 3)). Fare: list(batch(xrange(10), 3)produrrà risultati inaspettati.
Nathan Buesgens,

2
Non funziona su py3. .next()deve essere cambiato in next(..), e list(batch(range(0,10),3))lanciaRuntimeError: generator raised StopIteration
mathieu

1
@mathieu: avvolgi il whileciclo in try:/ except StopIteration: returnper risolvere quest'ultimo problema.
ShadowRanger

13

Ho solo dato una risposta. Tuttavia, ora sento che la soluzione migliore potrebbe essere non scrivere nuove funzioni. More-itertools include molti strumenti aggiuntivi ed chunkedè tra questi.


Questa è davvero la risposta più adatta (anche se richiede l'installazione di un altro pacchetto), e c'è anche ichunkedche produce iterabili.
viddik13

10

Strano, sembra funzionare bene per me in 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]

Ottima risposta perché non ha bisogno di importare nulla ed è intuitivo da leggere.
ojunk

8

Questo è uno snippet di codice molto breve che so che non usa lene funziona sia con Python 2 che con 3 (non di mia creazione):

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

7

Soluzione per Python 3.8 se stai lavorando con iterabili che non definiscono una lenfunzione e ti esaurisci:

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

Utilizzo di esempio:

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]

Ovviamente potrebbe essere implementato anche senza l'operatore tricheco.


3
Nella versione corrente, batcheraccetta un iteratore, non un iterabile. Risulterebbe in un ciclo infinito con un elenco, per esempio. Probabilmente dovrebbe esserci una linea iterator = iter(iterable)prima di iniziare il whileciclo.
Daniel Perez

2

Questo è ciò che utilizzo nel mio progetto. Gestisce iterabili o elenchi nel modo più efficiente possibile.

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

La migliore risposta finora, funziona con tutte le strutture dati
Clément Prévost

1

Questo funzionerebbe per qualsiasi iterabile.

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

Funzionerebbe così:

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

PS: non funzionerebbe se iterable avesse valori None.


1

Ecco un approccio che utilizza reduce funzione.

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

O versione più leggibile:

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

Test:

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

1

Una versione praticabile senza nuove funzionalità in python 3.8, adattata dalla risposta di @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)

Produzione:

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

0

Puoi semplicemente raggruppare gli elementi iterabili in base al loro indice batch.

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

È spesso il caso in cui si desidera raccogliere iterabili interni, quindi ecco una versione più avanzata.

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

Esempi:

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

Funzionalità correlate di cui potresti aver bisogno:

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

Utilizzo:

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

Ottiene l'i-esimo batch dalla sequenza e può funzionare anche con altre strutture di dati, come pandas dataframes ( df.iloc[batch(100,0)]) o 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

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

0

Continua a prendere (al massimo) n elementi finché non si esaurisce.

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.