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.