¿Iterar sobre una secuencia de python en múltiplos de n?

¿Cómo puedo procesar los elementos de una secuencia en lotes, idiomáticamente?

Por ejemplo, con la secuencia “abcdef” y un tamaño de lote de 2, me gustaría hacer algo como lo siguiente:

for x, y in "abcdef": print "%s%s\n" % (x, y) ab cd ef 

Por supuesto, esto no funciona porque está esperando un solo elemento de la lista que a su vez contiene 2 elementos.

¿Cuál es una manera agradable, corta, limpia y pirónica de procesar los siguientes n elementos de una lista en un lote o subcadenas de longitud n desde una cadena más grande (dos problemas similares)?

Estoy seguro de que a alguien se le ocurrirá algo más de “Pythonic” pero, ¿qué tal?

 for y in range(0, len(x), 2): print "%s%s" % (x[y], x[y+1]) 

Tenga en cuenta que esto solo funcionaría si sabe que len(x) % 2 == 0;

Una función de generador estaría limpia:

 def batch_gen(data, batch_size): for i in range(0, len(data), batch_size): yield data[i:i+batch_size] 

Ejemplo de uso:

 a = "abcdef" for i in batch_gen(a, 2): print i 

huellas dactilares:

 ab cd ef 

Tengo un enfoque alternativo, que funciona para iterables que no tienen una longitud conocida.

 def groupsgen(seq, size): it = iter(seq) while True: values = () for n in xrange(size): values += (it.next(),) yield values 

Funciona al iterar sobre la secuencia (u otro iterador) en grupos de tamaño, recolectando los valores en una tupla. Al final de cada grupo, cede la tupla.

Cuando el iterador se queda sin valores, produce una excepción StopIteration que luego se propaga hacia arriba, lo que indica que groupgen está fuera de valores.

Se supone que los valores vienen en conjuntos de tamaño (conjuntos de 2, 3, etc.). Si no, los valores que quedan se descartan.

No te olvides de la función zip ():

 a = 'abcdef' for x,y in zip(a[::2], a[1::2]): print '%s%s' % (x,y) 

pero la forma más general sería (inspirada en esta respuesta ):

 for i in zip(*(seq[i::size] for i in range(size))): print(i) # tuple of individual values 

Y luego siempre está la documentación .

 def pairwise(iterable): "s -> (s0,s1), (s1,s2), (s2, s3), ..." a, b = tee(iterable) try: b.next() except StopIteration: pass return izip(a, b) def grouper(n, iterable, padvalue=None): "grouper(3, 'abcdefg', 'x') --> ('a','b','c'), ('d','e','f'), ('g','x','x')" return izip(*[chain(iterable, repeat(padvalue, n-1))]*n) 

Nota: estos producen tuplas en lugar de subcadenas, cuando se les da una secuencia de cadena como entrada.

 >>> a = "abcdef" >>> size = 2 >>> [a[x:x+size] for x in range(0, len(a), size)] ['ab', 'cd', 'ef'] 

..o, no como una lista de comprensión:

 a = "abcdef" size = 2 output = [] for x in range(0, len(a), size): output.append(a[x:x+size]) 

O, como generador, sería mejor si se usara varias veces (para un solo uso, la comprensión de la lista es probablemente “la mejor”):

 def chunker(thelist, segsize): for x in range(0, len(thelist), segsize): yield thelist[x:x+segsize] 

..y su uso:

 >>> for seg in chunker(a, 2): ... print seg ... ab cd ef 

puedes crear el siguiente generador

 def chunks(seq, size): a = range(0, len(seq), size) b = range(size, len(seq) + 1, size) for i, j in zip(a, b): yield seq[i:j] 

y úsalo así:

 for i in chunks('abcdef', 2): print(i) 

De los documentos de more_itertools:

 more_itertools.chunked(iterable, n) 

Divide un iterable en listas de una longitud dada:

 >>> list(chunked([1, 2, 3, 4, 5, 6, 7], 3)) [[1, 2, 3], [4, 5, 6], [7]] 

Si la longitud de iterable no es divisible por n, la última lista devuelta será más corta.

 s = 'abcdefgh' for e in (s[i:i+2] for i in range(0,len(s),2)): print(e) 

El doc itertol tiene una receta para esto:

 from itertools import izip_longest def grouper(iterable, n, fillvalue=None): "Collect data into fixed-length chunks or blocks" # grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx args = [iter(iterable)] * n return izip_longest(fillvalue=fillvalue, *args) 

Uso:

 >>> l = [1,2,3,4,5,6,7,8,9] >>> [z for z in grouper(l, 3)] [(1, 2, 3), (4, 5, 6), (7, 8, 9)] 

Una solución, aunque desafío a alguien para hacerlo mejor 😉

 a = 'abcdef' b = [[a[i-1], a[i]] for i in range(1, len(a), 2)] for x, y in b: print "%s%s\n" % (x, y) 

¿Qué tal itertools?

 from itertools import islice, groupby def chunks_islice(seq, size): while True: aux = list(islice(seq, 0, size)) if not aux: break yield "".join(aux) def chunks_groupby(seq, size): for k, chunk in groupby(enumerate(seq), lambda x: x[0] / size): yield "".join([i[1] for i in chunk]) 

Excepto por dos respuestas, vi mucha materialización prematura en los lotes y suscripciones (que no funciona para todos los iteradores). De ahí se me ocurrió esta alternativa:

 def iter_x_and_n(iterable, x, n): yield x try: for _ in range(n): yield next(iterable) except StopIteration: pass def batched(iterable, n): if n<1: raise ValueError("Can not create batches of size %d, number must be strictly positive" % n) iterable = iter(iterable) try: for x in iterable: yield iter_x_and_n(iterable, x, n-1) except StopIteration: pass 

Me gana la pena que no haya una solución de una sola línea o de pocas líneas para esto (hasta donde sé). El problema clave es que tanto el generador externo como el interno necesitan manejar la StopIteration correctamente. El generador externo solo debe producir algo si queda al menos un elemento. La forma intuitiva de verificar esto, es ejecutar un próximo (...) y capturar una StopIteration.

Dado

 from __future__ import print_function # python 2.x seq = "abcdef" n = 2 

Código

 while seq: print("{}".format(seq[:n]), end="\n") seq = seq[n:] 

Salida

 ab cd ef