Pregunta

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 así sería mejor?

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

Todavía no se siente " siente " bien, sin embargo. : - /

Pregunta relacionada: Cómo hacer dividiste una lista en partes iguales en Python?

¿Fue útil?

Solución

Modificado de la sección recetas de la sección itertools docs:

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

Otros consejos

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

Soy fanático 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)

Otra forma:

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

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ás el último fragmento parcial. Si desea obtener (9, None, None, None) como último fragmento, simplemente use izip_longest de itertools .

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

Utilizando el % timeit de ipython en mi Mac Book Air, obtengo 47.5 us por ciclo.

Sin embargo, esto realmente no funciona para mí, ya que los resultados se rellenan para ser grupos de tamaño par. 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 se me ocurre utiliza islice para el 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 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 ciclo

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 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 Respuesta de @ ??O????? :

>>> 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 () soluciona el problema de relleno en la respuesta de J.F. 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 . Admite todos los iterables (no solo las 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 admite el llenado (el último grupo incompleto) o el truncamiento (descartando 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 de mayor rendimiento para hacerlo será utilizar 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 atrae; Prefiero usar 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 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 Markus Jarderot & # 8217; respuesta de):

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)

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

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

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 de rendimiento, así que no sé cuál podría ser más eficiente.

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

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 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 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 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) 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, p. para c en la tupla (ii) , o establezca 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 partición o función de fragmentos de 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 iteradoras ipartition y ichunks , que serán más eficientes en este caso.

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

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

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

Acerca de 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, puede escribir '' .join (chunker (...)) , pero la tupla temporal se construye de todos modos.

Puede deshacerse de la tupla temporal escribiendo su propio 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'

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

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 relleno debido a la necesidad de comparar y filtrar los valores rellenados. Sin embargo, para tamaños de fragmentos 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 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')]

No parece haber una buena manera de hacer esto. Aquí es 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):
    ...

Solución ad hoc 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 cadenas en subcadenas para analizar cadenas que contengan hexadecimal.
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 contacta / genera / itera sobre datos de entrada,
  • size es, por supuesto, el tamaño del fragmento que desea obtener,

Más interesante

  • reductor es invocable, que recibe el generador iterando sobre el contenido del fragmento.
    Esperaría que devuelva secuencia o cadena, pero no lo exijo.

    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 reductor puede causar el cierre del generador al generar una excepción.

  • condition es un llamado que recibe cualquier cosa que reductor devolvió.
    Decide aprobar & amp; cederlo (devolviendo cualquier cosa que evalúe a True ),
    o para rechazarlo & amp; 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 se agota, reductor lo hará generador de recepción que genera menos elementos que size .
    Llamemos a estos elementos últimos elementos .

    Invité a pasar dos funciones como este argumento:

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

    • lambda x: len (x) == < size > - los últimos elementos serán rechazados.
      reemplace < size > con un número igual a size

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top