cómo dividir un iterable en trozos de tamaño constante

85

Posible duplicado:
¿Cómo se divide una lista en fragmentos de tamaño uniforme en Python?

Me sorprende no haber podido encontrar una función "por lotes" que tome como entrada un iterable y devuelva un iterable de iterables.

Por ejemplo:

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]

Ahora, escribí lo que pensé que era un generador bastante simple:

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

Pero lo anterior no me da lo que hubiera esperado:

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]

Entonces, me he perdido algo y esto probablemente muestra mi total falta de comprensión de los generadores de Python. ¿A alguien le importaría indicarme la dirección correcta?

[Editar: finalmente me di cuenta de que el comportamiento anterior ocurre solo cuando ejecuto esto dentro de ipython en lugar de python en sí]

Mathieu
fuente
Buena pregunta, bien escrita, pero ya existe y solucionará tu problema.
Josh Smeaton
7
En mi opinión, esto no es realmente un duplicado. La otra pregunta se centra en listas en lugar de iteradores, y la mayoría de esas respuestas requieren len (), lo cual no es deseable para los iteradores. Pero eh, la respuesta actualmente aceptada aquí también requiere len (), así que ...
dequis
7
Claramente, esto no es un duplicado. Las otras preguntas y respuestas solo funcionan para listas , y esta pregunta se trata de generalizar a todos los iterables, que es exactamente la pregunta que tenía en mente cuando vine aquí.
Mark E. Haase
1
@JoshSmeaton @casperOne esto no es un duplicado y la respuesta aceptada no es correcta. La pregunta duplicada vinculada es para lista y esto es iterable. list proporciona el método len () pero iterable no proporciona un método len () y la respuesta sería diferente sin usar len () Esta es la respuesta correcta: batch = (tuple(filterfalse(lambda x: x is None, group)) for group in zip_longest(fillvalue=None, *[iter(iterable)] * n))
Trideep Rath
@TrideepRath sí, he votado para reabrir.
Josh Smeaton

Respuestas:

117

Probablemente sea más eficiente (más 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

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

Evita la creación de nuevas listas.

Carl F.
fuente
4
Para que conste, esta es la solución más rápida que encontré: mío = 4.5s, tuyo = 0.43s, Donkopotamus = 14.8s
mathieu
74
su lote de hecho acepta una lista (con len ()), no iterable (sin len ())
tdihp
28
Esto es más rápido porque no es una solución al problema. La receta del mero de Raymond Hettinger, actualmente debajo de esto, es lo que está buscando para una solución general que no requiera que el objeto de entrada tenga un método len .
Robert E Mealey
7
¿Por qué usa min ()? ¡Sin min()código es completamente correcto!
Pavel Patrin
20
Los iterables no tienen len(), las secuencias tienenlen()
Kos
60

FWIW, las recetas en el módulo itertools proporcionan este ejemplo:

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

Funciona así:

>>> list(grouper(3, range(10)))
[(0, 1, 2), (3, 4, 5), (6, 7, 8), (9, None, None)]
Raymond Hettinger
fuente
13
Esto no es exactamente lo que necesitaba, ya que rellena el último elemento con un conjunto de Ninguno. es decir, Ninguno es un valor válido en los datos que realmente uso con mi función, así que lo que necesito es algo que no rellene la última entrada.
mathieu
12
@mathieu Reemplazar izip_longestcon izip, que no rellenará las últimas entradas, sino que cortará las entradas cuando algunos de los elementos comiencen a agotarse.
GoogieK
3
Debería ser zip_longest / zip en python 3
Peter Gerdes
5
@GoogieK de for x, y in enumerate(grouper(3, xrange(10))): print(x,y)hecho no completa los valores, simplemente elimina el segmento incompleto por completo.
kadrach
3
Como un trazador de líneas que cae el último elemento si incompleta: list(zip(*[iter(iterable)] * n)). Este tiene que ser el código Python más bonito que he visto en mi vida.
Le Frite
31

Como han señalado otros, el código que ha proporcionado hace exactamente lo que desea. Para otro enfoque itertools.islice, puede ver un ejemplo de la siguiente receta:

from itertools import islice, chain

def batch(iterable, size):
    sourceiter = iter(iterable)
    while True:
        batchiter = islice(sourceiter, size)
        yield chain([batchiter.next()], batchiter)
donkopotamus
fuente
1
@abhilash No ... este código usa la llamada a next()para hacer que se agote una StopIterationvez sourceiter, terminando así el iterador. Sin la llamada next, continuaría devolviendo iteradores vacíos indefinidamente.
donkopotamus
7
Tuve que reemplazar batchiter.next()con next(batchiter)para que el código anterior funcione en Python 3.
Martin Wiebusch
2
señalando un comentario del artículo vinculado: "Debe agregar una advertencia de que un lote debe consumirse por completo antes de poder continuar con el siguiente". La salida de este debe ser consumido con algo como: map(list, batch(xrange(10), 3)). Hacer: list(batch(xrange(10), 3)producirá resultados inesperados.
Nathan Buesgens
2
No funciona en py3. .next()debe cambiarse a next(..)y list(batch(range(0,10),3))arrojaRuntimeError: generator raised StopIteration
mathieu
1
@mathieu: Envuelva el whilebucle en try:/ except StopIteration: returnpara solucionar este último problema.
ShadowRanger
13

Solo di una respuesta. Sin embargo, ahora creo que la mejor solución podría ser no escribir funciones nuevas. More-itertools incluye muchas herramientas adicionales y se chunkedencuentra entre ellas.

Yongwei Wu
fuente
De hecho, esta es la respuesta más adecuada (a pesar de que requiere la instalación de un paquete más), y también hay ichunkediterables.
viddik13
10

Extraño, parece funcionar bien para mí en 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
fuente
Gran respuesta porque no necesita importar nada y es intuitivo de leer.
ojunk
8

Este es un fragmento de código muy corto que sé que no se usa leny funciona tanto en Python 2 como en 3 (no es mi creación):

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
fuente
4

Solución para Python 3.8 si está trabajando con iterables que no definen una lenfunción y se agotan:

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

Uso de ejemplo:

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]

Por supuesto, también podría implementarse sin el operador de morsa.

Atra Azami
fuente
1
En la versión actual, batcheracepta un iterador, no un iterable. Daría como resultado un bucle infinito con una lista, por ejemplo. Probablemente debería haber una línea iterator = iter(iterable)antes de comenzar el whileciclo.
Daniel Perez
2

Esto es lo que uso en mi proyecto. Maneja iterables o listas de la manera más eficiente posible.

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
fuente
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
fuente
La mejor respuesta hasta ahora, funciona con todas las estructuras de datos
Clément Prévost
1

Esto funcionaría para cualquier iterable.

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

Funcionaría así:

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

PD: No funcionaría si iterable tiene valores None.

Trideep Rath
fuente
1

Aquí hay un enfoque que usa la reducefunción.

Un trazador de líneas:

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 una versión más legible:

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

Prueba:

>>> 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
fuente
0

Puede agrupar elementos iterables por su í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

A menudo es el caso cuando desea recopilar iterables internos, por lo que aquí hay una versión más avanzada.

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

Ejemplos:

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]]
dimathe47
fuente
0

Funcionalidad relacionada que puede necesitar:

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]

Obtiene el primer lote de la secuencia y también puede funcionar con otras estructuras de datos, como pandas dataframes ( df.iloc[batch(100,0)]) o numpy array ( array[batch(100,0)]).

alvitawa
fuente
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
fuente
0

yo suelo

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
fuente
0

Siga tomando (como máximo) n elementos hasta que se 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
fuente