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


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.


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

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)

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.


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]

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

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.


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

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


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

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

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


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

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

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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.