¿Cuál es la forma más "pitónica" de iterar sobre una lista en fragmentos?


488

Tengo un script de Python que toma como entrada una lista de enteros, que necesito para trabajar con cuatro enteros a la vez. Desafortunadamente, no tengo control de la entrada, o me la pasaría como una lista de tuplas de cuatro elementos. Actualmente, estoy iterando sobre esto de esta manera:

for i in xrange(0, len(ints), 4):
    # dummy op for example code
    foo += ints[i] * ints[i + 1] + ints[i + 2] * ints[i + 3]

Sin embargo, se parece mucho a "C-think", lo que me hace sospechar que hay una forma más pitónica de lidiar con esta situación. La lista se descarta después de iterar, por lo que no es necesario conservarla. ¿Quizás algo como esto sería mejor?

while ints:
    foo += ints[0] * ints[1] + ints[2] * ints[3]
    ints[0:4] = []

Sin embargo, todavía no se "siente" bien. : - /

Pregunta relacionada: ¿Cómo se divide una lista en partes iguales en Python?


3
Su código no funciona si el tamaño de la lista no es múltiplo de cuatro.
Pedro Henriques

55
Estoy extendiendo () la lista para que su longitud sea un múltiplo de cuatro antes de llegar tan lejos.
Ben Blank

44
@ ΤΖΩΤΖΙΟΥ: las preguntas son muy similares, pero no del todo duplicadas. Se "divide en cualquier número de fragmentos de tamaño N" frente a "se divide en N fragmentos de cualquier tamaño". :-)
Ben Blank


1
Posible duplicado del generador
Cristian Ciupitu

Respuestas:


340

Modificado de la sección de recetas de los documentos de itertools de Python :

from itertools import zip_longest

def grouper(iterable, n, fillvalue=None):
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)

Ejemplo
en pseudocódigo para mantener el ejemplo conciso.

grouper('ABCDEFG', 3, 'x') --> 'ABC' 'DEF' 'Gxx'

Nota: en Python 2 use en izip_longestlugar dezip_longest .


67
Finalmente tuve la oportunidad de jugar con esto en una sesión de Python. Para aquellos que están tan confundidos como yo, esto está alimentando al mismo iterador para izip_longest varias veces, lo que hace que consuma valores sucesivos de la misma secuencia en lugar de valores rayados de secuencias separadas. ¡Me encanta!
Ben Blank

66
¿Cuál es la mejor manera de filtrar el valor de relleno? ([ítem por ítem en ítems si el ítem no tiene valor de relleno] para ítems en mero (iterable))?
gotgenes

14
Sospecho que el rendimiento de esta receta de mero para trozos de 256k será muy pobre, porque izip_longestse alimentará con 256k argumentos.
anatoly techtonik

13
En varios lugares, los comentaristas dicen "cuando finalmente descubrí cómo funcionó esto ..." Tal vez se requiera un poco de explicación. Particularmente el aspecto de la lista de iteradores.
LondonRob

66
¿Hay alguna manera de usar esto pero sin Nonellenar el último fragmento?
CMCDragonkai

420
def chunker(seq, size):
    return (seq[pos:pos + size] for pos in range(0, len(seq), size))
# (in python 2 use xrange() instead of range() to avoid allocating a list)

Simple. Fácil. Rápido. Funciona con cualquier secuencia:

text = "I am a very, very helpful text"

for group in chunker(text, 7):
   print repr(group),
# 'I am a ' 'very, v' 'ery hel' 'pful te' 'xt'

print '|'.join(chunker(text, 10))
# I am a ver|y, very he|lpful text

animals = ['cat', 'dog', 'rabbit', 'duck', 'bird', 'cow', 'gnu', 'fish']

for group in chunker(animals, 3):
    print group
# ['cat', 'dog', 'rabbit']
# ['duck', 'bird', 'cow']
# ['gnu', 'fish']

16
La versión de @Carlos Crasborn funciona para cualquier iterable (no solo secuencias como el código anterior); es conciso y probablemente igual de rápido o incluso más rápido. Aunque puede ser un poco oscuro (poco claro) para las personas que no están familiarizadas con el itertoolsmódulo.
jfs el

1
Convenido. Esta es la forma más genérica y pitónica. Claro y conciso. (y funciona en el motor de aplicaciones)
Matt Williamson

3
Tenga en cuenta que chunkerdevuelve a generator. Reemplace el retorno a: return [...]para obtener una lista.
Dror

11
En lugar de escribir un edificio de función y luego regresar un generador, también se podría escribir un generador directamente, mediante yield: for pos in xrange(0, len(seq), size): yield seq[pos:pos + size]. No estoy seguro si internamente esto se manejaría de manera diferente en cualquier aspecto relevante, pero podría ser incluso un poco más claro.
Alfe

3
Tenga en cuenta que esto solo funciona para secuencias que admiten el acceso a elementos por índice y no funcionará para iteradores genéricos, ya que es posible que no admitan el __getitem__método.
apollov

135

Soy un fan de

chunk_size= 4
for i in range(0, len(ints), chunk_size):
    chunk = ints[i:i+chunk_size]
    # process chunk of size <= chunk_size

¿Cómo se comporta si len (ints) no es un múltiplo de chunkSize?
PlsWork

3
@AnnaVopureta chunktendrá 1, 2 o 3 elementos para el último lote de elementos. Vea esta pregunta sobre por qué los índices de corte pueden estar fuera de los límites .
Boris

22
import itertools
def chunks(iterable,size):
    it = iter(iterable)
    chunk = tuple(itertools.islice(it,size))
    while chunk:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

# though this will throw ValueError if the length of ints
# isn't a multiple of four:
for x1,x2,x3,x4 in chunks(ints,4):
    foo += x1 + x2 + x3 + x4

for chunk in chunks(ints,4):
    foo += sum(chunk)

De otra manera:

import itertools
def chunks2(iterable,size,filler=None):
    it = itertools.chain(iterable,itertools.repeat(filler,size-1))
    chunk = tuple(itertools.islice(it,size))
    while len(chunk) == size:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

# x2, x3 and x4 could get the value 0 if the length is not
# a multiple of 4.
for x1,x2,x3,x4 in chunks2(ints,4,0):
    foo += x1 + x2 + x3 + x4

2
+1 por usar generadores, parece la solución más "pitónica" de todas las soluciones sugeridas
Sergey Golovchenko

77
Es bastante largo y torpe para algo tan fácil, que no es muy pitónico. Prefiero la versión de S. Lott
zenazn

44
@zenazn: esto funcionará en instancias de generador, el corte no lo hará
Janus Troelsen

Además de funcionar correctamente con generadores y otros iteradores que no se pueden cortar, la primera solución tampoco requiere un valor de "relleno" si el fragmento final es menor que size, lo que a veces es deseable.
dano

1
También +1 para generadores. Otras soluciones requieren una lenllamada y, por lo tanto, no funcionan en otros generadores.
Cuadue


11

La solución ideal para este problema funciona con iteradores (no solo secuencias). También debería ser rápido.

Esta es la solución proporcionada por la documentación para itertools:

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

Usando ipython %timeiten mi Mac Book Air, obtengo 47.5 us por bucle.

Sin embargo, esto realmente no funciona para mí, ya que los resultados se rellenan para ser grupos de tamaño uniforme. Una solución sin el relleno es un poco más complicada. La solución más ingenua podría ser:

def grouper(size, iterable):
    i = iter(iterable)
    while True:
        out = []
        try:
            for _ in range(size):
                out.append(i.next())
        except StopIteration:
            yield out
            break

        yield out

Simple, pero bastante lento: 693 us por ciclo

La mejor solución que podría encontrar para usos islicedel bucle interno:

def grouper(size, iterable):
    it = iter(iterable)
    while True:
        group = tuple(itertools.islice(it, None, size))
        if not group:
            break
        yield group

Con el mismo conjunto de datos, obtengo 305 us por ciclo.

Al no poder obtener una solución pura más rápido que eso, proporciono la siguiente solución con una advertencia importante: si sus datos de entrada tienen instancias filldata, podría obtener una respuesta incorrecta.

def grouper(n, iterable, fillvalue=None):
    #"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
    args = [iter(iterable)] * n
    for i in itertools.izip_longest(fillvalue=fillvalue, *args):
        if tuple(i)[-1] == fillvalue:
            yield tuple(v for v in i if v != fillvalue)
        else:
            yield i

Realmente no me gusta esta respuesta, pero es significativamente más rápida. 124 us por lazo


Se puede reducir el tiempo de ejecución para la receta # 3 por ~ 10-15% moviéndolo a la capa C (omitiendo itertoolslas importaciones; mapdebe ser el AP3 mapo imap): def grouper(n, it): return takewhile(bool, map(tuple, starmap(islice, repeat((iter(it), n))))). Su función final puede hacerse menos frágil utilizando un centinela: deshacerse del fillvalueargumento; agregue una primera línea fillvalue = object(), luego cambie el ifcheque a if i[-1] is fillvalue:y la línea a la que controla yield tuple(v for v in i if v is not fillvalue). Garantías que ningún valor iterablepuede confundirse con el valor de relleno.
ShadowRanger

Por cierto, un gran aprobado en el n. ° 4. Estaba a punto de publicar mi optimización del n. ° 3 como una mejor respuesta (en cuanto al rendimiento) que lo que se había publicado hasta ahora, pero con el ajuste para que sea confiable, el n. ° 4 resistente supera el doble de rápido que el n. ° 3 optimizado; No esperaba una solución con bucles de nivel Python (y sin diferencias algorítmicas teóricas AFAICT) para ganar. Supongo que el n. ° 3 pierde debido al gasto de construcción / iteración de isliceobjetos (el n. ° 3 gana si nes relativamente grande, por ejemplo, el número de grupos es pequeño, pero eso es optimizar para un caso poco común), pero no esperaba que fuera bastante extremo.
ShadowRanger

Para el n. ° 4, la primera rama del condicional solo se toma en la última iteración (la tupla final). En vez de reconstituir la tupla final de todo de nuevo, almacenar en caché el módulo de la longitud de la iterables originales en la parte superior y el uso que para rebanar el relleno no deseado de izip_longestla tupla final: yield i[:modulo]. Además, para la argsvariable tupla en lugar de una lista: args = (iter(iterable),) * n. Afeita algunos ciclos de reloj más. Por último, si ignoramos el valor de relleno y asumimos None, el condicional puede convertirse if None in ien incluso más ciclos de reloj.
Kumba

1
@Kumba: Su primera sugerencia asume que la entrada tiene longitud conocida. Si es un iterador / generador, no una colección con una longitud conocida, no hay nada que almacenar en caché. No hay ninguna razón real para usar una optimización de este tipo; está optimizando el caso poco común (el último yield), mientras que el caso común no se ve afectado.
ShadowRanger

10

Necesitaba una solución que también funcionara con conjuntos y generadores. No se me ocurrió nada muy corto y bonito, pero al menos es bastante legible.

def chunker(seq, size):
    res = []
    for el in seq:
        res.append(el)
        if len(res) == size:
            yield res
            res = []
    if res:
        yield res

Lista:

>>> list(chunker([i for i in range(10)], 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

Conjunto:

>>> list(chunker(set([i for i in range(10)]), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

Generador:

>>> list(chunker((i for i in range(10)), 3))
[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]

8

Similar a otras propuestas, pero no exactamente idéntico, me gusta hacerlo de esta manera, porque es simple y fácil de leer:

it = iter([1, 2, 3, 4, 5, 6, 7, 8, 9])
for chunk in zip(it, it, it, it):
    print chunk

>>> (1, 2, 3, 4)
>>> (5, 6, 7, 8)

De esta manera no obtendrá el último fragmento parcial. Si desea obtener el (9, None, None, None)último fragmento, solo use izip_longestde itertools.


se puede mejorar conzip(*([it]*4))
Jean-François Fabre

@ Jean-François Fabre: desde el punto de vista de la legibilidad, no lo veo como una mejora. Y también es marginalmente más lento. Es una mejora si estás jugando al golf, y yo no.
kriss el

no, no estoy jugando al golf, pero ¿qué pasa si tienes 10 argumentos? Leí esa construcción en alguna página oficial. Pero, por supuesto, parece que no puedo encontrarla ahora :)
Jean-François Fabre

@ Jean-François Fabre: si tengo 10 argumentos, o un número variable de argumentos, es una opción, pero prefiero escribir: zip (* (it,) * 10)
kriss

¡Correcto! Eso es lo que leo. no es la lista de cosas que he hecho :)
Jean-François Fabre

8

Si no le importa usar un paquete externo, puede usar iteration_utilities.grouperdesde 1 . Admite todos los iterables (no solo las secuencias):iteration_utilties

from iteration_utilities import grouper
seq = list(range(20))
for group in grouper(seq, 4):
    print(group)

que imprime:

(0, 1, 2, 3)
(4, 5, 6, 7)
(8, 9, 10, 11)
(12, 13, 14, 15)
(16, 17, 18, 19)

En caso de que la longitud no sea un múltiplo del tamaño de grupo, también admite llenar (el último grupo incompleto) o truncar (descartar el último grupo incompleto) el último:

from iteration_utilities import grouper
seq = list(range(17))
for group in grouper(seq, 4):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)
# (16,)

for group in grouper(seq, 4, fillvalue=None):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)
# (16, None, None, None)

for group in grouper(seq, 4, truncate=True):
    print(group)
# (0, 1, 2, 3)
# (4, 5, 6, 7)
# (8, 9, 10, 11)
# (12, 13, 14, 15)

Puntos de referencia

También decidí comparar el tiempo de ejecución de algunos de los enfoques mencionados. Es una trama log-log que se agrupa en grupos de elementos "10" en función de una lista de diferentes tamaños. Para resultados cualitativos: Menor significa más rápido:

ingrese la descripción de la imagen aquí

Al menos en este punto de referencia, el iteration_utilities.groupermejor rendimiento. Seguido por el enfoque de Craz .

El punto de referencia fue creado con 1 . El código utilizado para ejecutar este punto de referencia fue:simple_benchmark

import iteration_utilities
import itertools
from itertools import zip_longest

def consume_all(it):
    return iteration_utilities.consume(it, None)

import simple_benchmark
b = simple_benchmark.BenchmarkBuilder()

@b.add_function()
def grouper(l, n):
    return consume_all(iteration_utilities.grouper(l, n))

def Craz_inner(iterable, n, fillvalue=None):
    args = [iter(iterable)] * n
    return zip_longest(*args, fillvalue=fillvalue)

@b.add_function()
def Craz(iterable, n, fillvalue=None):
    return consume_all(Craz_inner(iterable, n, fillvalue))

def nosklo_inner(seq, size):
    return (seq[pos:pos + size] for pos in range(0, len(seq), size))

@b.add_function()
def nosklo(seq, size):
    return consume_all(nosklo_inner(seq, size))

def SLott_inner(ints, chunk_size):
    for i in range(0, len(ints), chunk_size):
        yield ints[i:i+chunk_size]

@b.add_function()
def SLott(ints, chunk_size):
    return consume_all(SLott_inner(ints, chunk_size))

def MarkusJarderot1_inner(iterable,size):
    it = iter(iterable)
    chunk = tuple(itertools.islice(it,size))
    while chunk:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

@b.add_function()
def MarkusJarderot1(iterable,size):
    return consume_all(MarkusJarderot1_inner(iterable,size))

def MarkusJarderot2_inner(iterable,size,filler=None):
    it = itertools.chain(iterable,itertools.repeat(filler,size-1))
    chunk = tuple(itertools.islice(it,size))
    while len(chunk) == size:
        yield chunk
        chunk = tuple(itertools.islice(it,size))

@b.add_function()
def MarkusJarderot2(iterable,size):
    return consume_all(MarkusJarderot2_inner(iterable,size))

@b.add_arguments()
def argument_provider():
    for exp in range(2, 20):
        size = 2**exp
        yield size, simple_benchmark.MultiArgument([[0] * size, 10])

r = b.run()

1 Descargo de responsabilidad: soy el autor de las bibliotecas iteration_utilitiesy simple_benchmark.


7

Como nadie lo ha mencionado todavía, aquí hay una zip()solución:

>>> def chunker(iterable, chunksize):
...     return zip(*[iter(iterable)]*chunksize)

Funciona solo si la longitud de su secuencia siempre es divisible por el tamaño del fragmento o si no le importa un fragmento final si no lo es.

Ejemplo:

>>> s = '1234567890'
>>> chunker(s, 3)
[('1', '2', '3'), ('4', '5', '6'), ('7', '8', '9')]
>>> chunker(s, 4)
[('1', '2', '3', '4'), ('5', '6', '7', '8')]
>>> chunker(s, 5)
[('1', '2', '3', '4', '5'), ('6', '7', '8', '9', '0')]

O usando itertools.izip para devolver un iterador en lugar de una lista:

>>> from itertools import izip
>>> def chunker(iterable, chunksize):
...     return izip(*[iter(iterable)]*chunksize)

El relleno se puede arreglar usando la respuesta de @ ΤΖΩΤΖΙΟΥ :

>>> from itertools import chain, izip, repeat
>>> def chunker(iterable, chunksize, fillvalue=None):
...     it   = chain(iterable, repeat(fillvalue, chunksize-1))
...     args = [it] * chunksize
...     return izip(*args)

5

El uso de map () en lugar de zip () soluciona el problema de relleno en la respuesta de JF Sebastian:

>>> def chunker(iterable, chunksize):
...   return map(None,*[iter(iterable)]*chunksize)

Ejemplo:

>>> s = '1234567890'
>>> chunker(s, 3)
[('1', '2', '3'), ('4', '5', '6'), ('7', '8', '9'), ('0', None, None)]
>>> chunker(s, 4)
[('1', '2', '3', '4'), ('5', '6', '7', '8'), ('9', '0', None, None)]
>>> chunker(s, 5)
[('1', '2', '3', '4', '5'), ('6', '7', '8', '9', '0')]

2
Esto se maneja mejor con itertools.izip_longest(Py2) / itertools.zip_longest(Py3); este uso de mapes doblemente obsoleto y no está disponible en Py3 (no puede pasar Nonecomo la función de mapeador, y se detiene cuando se agota el iterable más corto, no el más largo; no se rellena).
ShadowRanger

4

Otro enfoque sería utilizar la forma de dos argumentos de iter:

from itertools import islice

def group(it, size):
    it = iter(it)
    return iter(lambda: tuple(islice(it, size)), ())

Esto se puede adaptar fácilmente para usar relleno (esto es similar a la respuesta de Markus Jarderot ):

from itertools import islice, chain, repeat

def group_pad(it, size, pad=None):
    it = chain(iter(it), repeat(pad))
    return iter(lambda: tuple(islice(it, size)), (pad,) * size)

Incluso se pueden combinar para un relleno opcional:

_no_pad = object()
def group(it, size, pad=_no_pad):
    if pad == _no_pad:
        it = iter(it)
        sentinel = ()
    else:
        it = chain(iter(it), repeat(pad))
        sentinel = (pad,) * size
    return iter(lambda: tuple(islice(it, size)), sentinel)

1
¡preferible porque tiene la opción de omitir el relleno!
n611x007

3

Si la lista es grande, la forma de mayor rendimiento para hacerlo será usar un generador:

def get_chunk(iterable, chunk_size):
    result = []
    for item in iterable:
        result.append(item)
        if len(result) == chunk_size:
            yield tuple(result)
            result = []
    if len(result) > 0:
        yield tuple(result)

for x in get_chunk([1,2,3,4,5,6,7,8,9,10], 3):
    print x

(1, 2, 3)
(4, 5, 6)
(7, 8, 9)
(10,)

(Creo que la sugerencia de itertools de MizardX es funcionalmente equivalente a esto.)
Robert Rossney el

1
(En realidad, al reflexionar, no, no lo hago. Itertools.islice devuelve un iterador, pero no usa uno existente.)
Robert Rossney el

Es agradable y simple, pero por alguna razón, incluso sin conversión a tupla 4-7 veces más lenta que el método de mero aceptado en iterable = range(100000000)y chunksizehasta 10000.
Valentas

Sin embargo, en general, recomendaría este método, porque el aceptado puede ser extremadamente lento cuando se verifica que el último elemento sea lento docs.python.org/3/library/itertools.html#itertools.zip_longest
Valentas

3

Usar pequeñas funciones y cosas realmente no me atrae; Prefiero usar solo rebanadas:

data = [...]
chunk_size = 10000 # or whatever
chunks = [data[i:i+chunk_size] for i in xrange(0,len(data),chunk_size)]
for chunk in chunks:
    ...

agradable pero no es bueno para una secuencia indefinida que no se conoce len. puedes hacer una prueba con itertools.repeato itertools.cycle.
n611x007

1
Además, consume memoria debido al uso de una [...for...] comprensión de la lista para construir físicamente una lista en lugar de usar una (...for...) expresión generadora que solo se preocuparía por el siguiente elemento y la memoria
adicional

2

Para evitar todas las conversiones a una lista import itertoolsy:

>>> for k, g in itertools.groupby(xrange(35), lambda x: x/10):
...     list(g)

Produce:

... 
0 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
2 [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
3 [30, 31, 32, 33, 34]
>>> 

Lo verifiqué groupbyy no se convierte en lista o usolen así que (creo) esto retrasará la resolución de cada valor hasta que realmente se use. Lamentablemente, ninguna de las respuestas disponibles (en este momento) parecía ofrecer esta variación.

Obviamente, si necesita manejar cada elemento a su vez, anide un bucle for sobre g:

for k,g in itertools.groupby(xrange(35), lambda x: x/10):
    for i in g:
       # do what you need to do with individual items
    # now do what you need to do with the whole group

Mi interés específico en esto fue la necesidad de consumir un generador para enviar cambios en lotes de hasta 1000 a la API de gmail:

    messages = a_generator_which_would_not_be_smart_as_a_list
    for idx, batch in groupby(messages, lambda x: x/1000):
        batch_request = BatchHttpRequest()
        for message in batch:
            batch_request.add(self.service.users().messages().modify(userId='me', id=message['id'], body=msg_labels))
        http = httplib2.Http()
        self.credentials.authorize(http)
        batch_request.execute(http=http)

¿Qué sucede si la lista que está fragmentando no es una secuencia de enteros ascendentes?
PaulMcG

@PaulMcGuire ver groupby ; dada una función para describir el orden, entonces los elementos del iterable pueden ser cualquier cosa, ¿verdad?
John Mee el

1
Sí, estoy familiarizado con groupby. Pero si los mensajes fueran las letras "ABCDEFG", entonces groupby(messages, lambda x: x/3)le daría un TypeError (por tratar de dividir una cadena por un int), no agrupaciones de 3 letras. Ahora, si lo hicieras groupby(enumerate(messages), lambda x: x[0]/3), podrías tener algo. Pero no dijiste eso en tu publicación.
PaulMcG

2

Con NumPy es simple:

ints = array([1, 2, 3, 4, 5, 6, 7, 8])
for int1, int2 in ints.reshape(-1, 2):
    print(int1, int2)

salida:

1 2
3 4
5 6
7 8

2
def chunker(iterable, n):
    """Yield iterable in chunk sizes.

    >>> chunks = chunker('ABCDEF', n=4)
    >>> chunks.next()
    ['A', 'B', 'C', 'D']
    >>> chunks.next()
    ['E', 'F']
    """
    it = iter(iterable)
    while True:
        chunk = []
        for i in range(n):
            try:
                chunk.append(next(it))
            except StopIteration:
                yield chunk
                raise StopIteration
        yield chunk

if __name__ == '__main__':
    import doctest

    doctest.testmod()

2

A menos que pierda algo, no se ha mencionado la siguiente solución simple con expresiones generadoras. Se supone que se conocen tanto el tamaño como la cantidad de fragmentos (que suele ser el caso), y que no se requiere relleno:

def chunks(it, n, m):
    """Make an iterator over m first chunks of size n.
    """
    it = iter(it)
    # Chunks are presented as tuples.
    return (tuple(next(it) for _ in range(n)) for _ in range(m))

1

En su segundo método, avanzaría al siguiente grupo de 4 haciendo esto:

ints = ints[4:]

Sin embargo, no he hecho ninguna medición de rendimiento, así que no sé cuál podría ser más eficiente.

Dicho esto, generalmente elegiría el primer método. No es bonito, pero eso es a menudo una consecuencia de la interacción con el mundo exterior.


1

Otra respuesta más, cuyas ventajas son:

1) Fácilmente comprensible
2) Funciona en cualquier secuencia iterable, no solo (algunas de las respuestas anteriores se ahogarán en los controladores de archivo)
3) No carga el fragmento en la memoria de una vez
4) No hace una lista de referencias largas el mismo iterador en la memoria
5) Sin relleno de valores de relleno al final de la lista

Dicho esto, no lo he cronometrado, por lo que podría ser más lento que algunos de los métodos más inteligentes, y algunas de las ventajas pueden ser irrelevantes dado el caso de uso.

def chunkiter(iterable, size):
  def inneriter(first, iterator, size):
    yield first
    for _ in xrange(size - 1): 
      yield iterator.next()
  it = iter(iterable)
  while True:
    yield inneriter(it.next(), it, size)

In [2]: i = chunkiter('abcdefgh', 3)
In [3]: for ii in i:                                                
          for c in ii:
            print c,
          print ''
        ...:     
        a b c 
        d e f 
        g h 

Actualización:
un par de inconvenientes debido al hecho de que los bucles interno y externo están extrayendo valores del mismo iterador:
1) continuar no funciona como se esperaba en el bucle externo: simplemente continúa al siguiente elemento en lugar de omitir un fragmento . Sin embargo, esto no parece ser un problema, ya que no hay nada que probar en el bucle externo.
2) el descanso no funciona como se esperaba en el bucle interno: el control terminará en el bucle interno nuevamente con el siguiente elemento en el iterador. Para omitir fragmentos enteros, envuelva el iterador interno (ii arriba) en una tupla, por ejemplo for c in tuple(ii), o establezca una bandera y agote el iterador.


1
def group_by(iterable, size):
    """Group an iterable into lists that don't exceed the size given.

    >>> group_by([1,2,3,4,5], 2)
    [[1, 2], [3, 4], [5]]

    """
    sublist = []

    for index, item in enumerate(iterable):
        if index > 0 and index % size == 0:
            yield sublist
            sublist = []

        sublist.append(item)

    if sublist:
        yield sublist

+1 omite el relleno; la suya y bcoughlan 's es muy similar
n611x007

1

Puede usar la función de partición o fragmentos de la biblioteca funcy :

from funcy import partition

for a, b, c, d in partition(4, ints):
    foo += a * b * c * d

Estas funciones también tiene iterador versiones ipartitiony ichunks, lo que será más eficiente en este caso.

También puede echar un vistazo a su implementación .


1

Sobre la solución dada por J.F. Sebastian aquí :

def chunker(iterable, chunksize):
    return zip(*[iter(iterable)]*chunksize)

Es inteligente, pero tiene una desventaja: siempre devuelve la tupla. ¿Cómo obtener una cuerda en su lugar?
Por supuesto que puedes escribir''.join(chunker(...)) , pero la tupla temporal se construye de todos modos.

Puedes deshacerte de la tupla temporal escribiendo la tuya propia zip, así:

class IteratorExhausted(Exception):
    pass

def translate_StopIteration(iterable, to=IteratorExhausted):
    for i in iterable:
        yield i
    raise to # StopIteration would get ignored because this is generator,
             # but custom exception can leave the generator.

def custom_zip(*iterables, reductor=tuple):
    iterators = tuple(map(translate_StopIteration, iterables))
    while True:
        try:
            yield reductor(next(i) for i in iterators)
        except IteratorExhausted: # when any of iterators get exhausted.
            break

Entonces

def chunker(data, size, reductor=tuple):
    return custom_zip(*[iter(data)]*size, reductor=reductor)

Ejemplo de uso:

>>> for i in chunker('12345', 2):
...     print(repr(i))
...
('1', '2')
('3', '4')
>>> for i in chunker('12345', 2, ''.join):
...     print(repr(i))
...
'12'
'34'

2
No es una crítica para que usted cambie su respuesta, sino un comentario: el código es una responsabilidad. Cuanto más código escriba, más espacio creará para que los errores se oculten. Desde este punto de vista, reescribir en ziplugar de usar el existente no parece ser la mejor idea.
Alfe

1

Me gusta este enfoque. Se siente simple y no mágico y admite todos los tipos iterables y no requiere importaciones.

def chunk_iter(iterable, chunk_size):
it = iter(iterable)
while True:
    chunk = tuple(next(it) for _ in range(chunk_size))
    if not chunk:
        break
    yield chunk

1

Nunca quiero mis pedazos acolchados, por lo que ese requisito es esencial. Creo que la capacidad de trabajar en cualquier iterable también es un requisito. Dado eso, decidí extender la respuesta aceptada, https://stackoverflow.com/a/434411/1074659 .

El rendimiento se ve afectado ligeramente en este enfoque si no se desea el relleno debido a la necesidad de comparar y filtrar los valores de relleno. Sin embargo, para trozos grandes, esta utilidad es muy eficaz.

#!/usr/bin/env python3
from itertools import zip_longest


_UNDEFINED = object()


def chunker(iterable, chunksize, fillvalue=_UNDEFINED):
    """
    Collect data into chunks and optionally pad it.

    Performance worsens as `chunksize` approaches 1.

    Inspired by:
        https://docs.python.org/3/library/itertools.html#itertools-recipes

    """
    args = [iter(iterable)] * chunksize
    chunks = zip_longest(*args, fillvalue=fillvalue)
    yield from (
        filter(lambda val: val is not _UNDEFINED, chunk)
        if chunk[-1] is _UNDEFINED
        else chunk
        for chunk in chunks
    ) if fillvalue is _UNDEFINED else chunks

1

Aquí hay un trozo sin importaciones que admite generadores:

def chunks(seq, size):
    it = iter(seq)
    while True:
        ret = tuple(next(it) for _ in range(size))
        if len(ret) == size:
            yield ret
        else:
            raise StopIteration()

Ejemplo de uso:

>>> def foo():
...     i = 0
...     while True:
...         i += 1
...         yield i
...
>>> c = chunks(foo(), 3)
>>> c.next()
(1, 2, 3)
>>> c.next()
(4, 5, 6)
>>> list(chunks('abcdefg', 2))
[('a', 'b'), ('c', 'd'), ('e', 'f')]

1

Con Python 3.8 puede usar el operador de morsa y itertools.islice.

from itertools import islice

list_ = [i for i in range(10, 100)]

def chunker(it, size):
    iterator = iter(it)
    while chunk := list(islice(iterator, size)):
        print(chunk)
In [2]: chunker(list_, 10)                                                         
[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
[30, 31, 32, 33, 34, 35, 36, 37, 38, 39]
[40, 41, 42, 43, 44, 45, 46, 47, 48, 49]
[50, 51, 52, 53, 54, 55, 56, 57, 58, 59]
[60, 61, 62, 63, 64, 65, 66, 67, 68, 69]
[70, 71, 72, 73, 74, 75, 76, 77, 78, 79]
[80, 81, 82, 83, 84, 85, 86, 87, 88, 89]
[90, 91, 92, 93, 94, 95, 96, 97, 98, 99]

0

No parece haber una buena manera de hacer esto. Aquí hay una página que tiene varios métodos, que incluyen:

def split_seq(seq, size):
    newseq = []
    splitsize = 1.0/size*len(seq)
    for i in range(size):
        newseq.append(seq[int(round(i*splitsize)):int(round((i+1)*splitsize))])
    return newseq

0

Si las listas son del mismo tamaño, puede combinarlas en listas de 4 tuplas con zip(). Por ejemplo:

# Four lists of four elements each.

l1 = range(0, 4)
l2 = range(4, 8)
l3 = range(8, 12)
l4 = range(12, 16)

for i1, i2, i3, i4 in zip(l1, l2, l3, l4):
    ...

Esto es lo zip()que produce la función:

>>> print l1
[0, 1, 2, 3]
>>> print l2
[4, 5, 6, 7]
>>> print l3
[8, 9, 10, 11]
>>> print l4
[12, 13, 14, 15]
>>> print zip(l1, l2, l3, l4)
[(0, 4, 8, 12), (1, 5, 9, 13), (2, 6, 10, 14), (3, 7, 11, 15)]

Si las listas son grandes y no desea combinarlas en una lista más grande, use itertools.izip(), que produce un iterador, en lugar de una lista.

from itertools import izip

for i1, i2, i3, i4 in izip(l1, l2, l3, l4):
    ...

0

Solución ad hoc de una sola línea para iterar sobre una lista xen trozos de tamaño 4:

for a, b, c, d in zip(x[0::4], x[1::4], x[2::4], x[3::4]):
    ... do something with a, b, c and d ...
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.