parts - yield python




¿Cuál es la forma más "pythonic" de iterar sobre una lista en trozos? (20)

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 pitónica 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?


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 generar una 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)==<size> - los últimos elementos serán rechazados.
      Reemplace <size> usando un número igual al size


Aquí hay un chunker sin importaciones que soporta generadores:

def chunks(seq, size):
    it = iter(seq)
    while True:
        ret = tuple(it.next() 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')]

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 a menudo es una consecuencia de la interfaz con el mundo exterior.


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


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

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

No parece haber una manera bonita de hacer esto. Here 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

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

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.


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)

Publicar esto como respuesta ya que no puedo comentar ...

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

Puede usar la función de partition o chunks 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 .


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

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 es compatible con 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.


Sobre la solución JF Sebastian por JF Sebastian here :

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'

Soy un fan de

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

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

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(it.next())
            except StopIteration:
                yield chunk
                raise StopIteration
        yield chunk

if __name__ == '__main__':
    import doctest

    doctest.testmod()

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

from itertools import izip_longest

def chunker(iterable, chunksize, filler):
    return izip_longest(*[iter(iterable)]*chunksize, fillvalue=filler)




chunks