¿Cuál es la forma más “pythonic” de iterar sobre una lista en trozos?

Tengo un script en 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 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 pythonica de lidiar con esta situación. La lista se desecha después de la iteración, por lo que no es necesario conservarla. Tal vez 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 de tamaño uniforme en Python?

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 izip_longest lugar de zip_longest .

 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) 

Sencillo. 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'] 

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 
 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 
 from itertools import izip_longest def chunker(iterable, chunksize, filler): return izip_longest(*[iter(iterable)]*chunksize, fillvalue=filler) 

Necesitaba una solución que también funcionara con conjuntos y generadores. No pude encontrar 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]] 

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

Esta es la solución provista 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) 

Al usar %timeit de %timeit en mi Mac Book Air, obtengo 47.5 por bucle.

Sin embargo, esto realmente no funciona para mí, ya que los resultados se rellenan para formar 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 bucle

La mejor solución que se me ocurre es el islice de 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 bucle.

Incapaz de 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 de datos de 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 bucle

Similar a otras propuestas, pero no exactamente idénticas, 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ás la última parte parcial. Si desea obtener (9, None, None, None) como el último fragmento, simplemente use izip_longest de itertools .

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

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

Solo funciona si la longitud de su secuencia siempre es divisible por el tamaño del fragmento o 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) 

El uso de map () en lugar de zip () corrige 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')] 

Si no le importa usar un paquete externo, puede usar iteration_utilities.grouper de iteration_utilties 1 . Soporta todos los iterables (no solo secuencias):

 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 se puede rellenar (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) 

1 Descargo de responsabilidad: Soy el autor de ese paquete.

Si la lista es grande, la forma más eficiente de 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,) 

Usar pequeñas funciones y cosas realmente no me atraen; 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: ... 

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

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

Estos incluso se pueden combinar para el 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) 

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 

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

 ints = ints[4:] 

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

Habiendo dicho eso, normalmente elegiría el primer método. No es bonito, pero eso es a menudo una consecuencia de la interfaz con el mundo exterior.

Otra respuesta más, cuyas ventajas son:

1) Fácilmente comprensible
2) Funciona en cualquier iterable, no solo en secuencias (algunas de las respuestas anteriores se ahogarán en los identificadores de archivos)
3) No carga el trozo en la memoria de una vez
4) No hace una lista larga de referencias al 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 en 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 '' ...: abcdefgh 

Actualizar:
Un par de inconvenientes debido a 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 con el 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) la ruptura no funciona como se esperaba en el bucle interno: el control terminará nuevamente en el bucle interno 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 coloque una bandera y agote el iterador.

 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 

Puede usar la función de partición o trozos 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 tienen versiones de iterador ipartition e ichunks , que serán más eficientes en este caso.

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

Para evitar que todas las conversiones a una lista import itertools y:

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

groupby y no se convierte a la lista ni usa len así que (creo) esto retrasará la resolución de cada valor hasta que 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) 

Sobre la solución JF Sebastian por JF Sebastian aquí :

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

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

Puedes deshacerte de la tupla temporal escribiendo tu propio zip , como este:

 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' 

Me gusta este enfoque. Se siente simple y no mágico y soporta 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 

Nunca quiero que me rellenen los trozos, por lo que ese requisito es esencial. Me parece que la capacidad de trabajar en cualquier iterable también es un requisito. Dado eso, decidí ampliar la respuesta aceptada, https://stackoverflow.com/a/434411/1074659 .

El rendimiento recibe un leve golpe en este enfoque si no se desea el relleno debido a la necesidad de comparar y filtrar los valores rellenados. Sin embargo, para tamaños de 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 
 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() 

Aquí hay un chunker sin importaciones que soporta 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')] 

No parece haber una manera bonita 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 

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 que produce la función zip() :

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

Una solución adhoc de una sola línea para iterar sobre una lista x en 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 principio, lo diseñé para dividir las cadenas en subcadenas para analizar la cadena que contiene hex.
Hoy lo convertí en un generador complejo, pero aún simple.

 def chunker(iterable, size, reductor, condition): it = iter(iterable) def chunk_generator(): return (next(it) for _ in range(size)) chunk = reductor(chunk_generator()) while condition(chunk): yield chunk chunk = reductor(chunk_generator()) 

Argumentos:

Los obvios

  • iterable es cualquier iterable / iterador / generador que contenga / genere / itere sobre datos de entrada,
  • size es, por supuesto, el tamaño del trozo que desea obtener,

Más interesante

  • reductor es un invocable, que recibe el generador iterando sobre el contenido del fragmento.
    Espero que devuelva secuencia o cadena, pero no exijo eso.

    Puede pasar como este argumento, por ejemplo, list , tuple , set , frozenset ,
    o cualquier cosa más elegante. Pasaría esta función, devolviendo cadena.
    (siempre que iterable contenga / genere / itere sobre cadenas):

     def concatenate(iterable): return ''.join(iterable) 

    Tenga en cuenta que el reductor puede provocar el cierre del generador al elevar la excepción.

  • condition es una llamada que recibe cualquier cosa que devuelva el reductor .
    Decide aprobarlo y cederlo (devolviendo todo lo que evalúa a True ),
    o rechazarlo y terminar el trabajo del generador (devolviendo cualquier otra cosa o generando una excepción).

    Cuando el número de elementos en iterable no es divisible por size , cuando it agota, el reductor recibirá un generador que genera menos elementos que size .
    Llamemos a estos elementos elementos de los últimos .

    Invité dos funciones para pasar como este argumento:

    • lambda x:x – los últimos elementos serán cedidos.

    • lambda x: len(x)== : los últimos elementos serán rechazados.
      Reemplace usando un número igual al size

Es fácil hacer que itertools.groupby trabaje para que usted obtenga un iterable de iterables, sin crear listas temporales:

 groupby(iterable, (lambda x,y: (lambda z: x.next()/y))(count(),100)) 

No se deje intimidar por las lambdas anidadas, lambda exterior se ejecuta solo una vez para poner el generador count() y la constante 100 en el scope de la lambda interna.

Yo uso esto para enviar trozos de filas a mysql.

 for k,v in groupby(bigdata, (lambda x,y: (lambda z: x.next()/y))(count(),100))): cursor.executemany(sql, v)