web-dev-qa-db-fra.com

Divisez un générateur en morceaux sans le faire marcher au préalable

(Cette question est liée à celle-ci et celle-ci , mais celles-ci font marcher le générateur, ce qui est exactement ce que je veux éviter)

Je voudrais diviser un générateur en morceaux. Les exigences sont les suivantes:

  • ne remplissez pas les morceaux: si le nombre d'éléments restants est inférieur à la taille du morceau, le dernier morceau doit être plus petit.
  • ne faites pas marcher le générateur au préalable: le calcul des éléments coûte cher, et cela ne doit être fait que par la fonction consommatrice, pas par le chunker
  • ce qui signifie bien sûr: ne pas accumuler en mémoire (pas de listes)

J'ai essayé le code suivant:

def head(iterable, max=10):
    for cnt, el in enumerate(iterable):
        yield el
        if cnt >= max:
            break

def chunks(iterable, size=10):
    i = iter(iterable)
    while True:
        yield head(i, size)

# Sample generator: the real data is much more complex, and expensive to compute
els = xrange(7)

for n, chunk in enumerate(chunks(els, 3)):
    for el in chunk:
        print 'Chunk %3d, value %d' % (n, el)

Et cela fonctionne en quelque sorte:

Chunk   0, value 0
Chunk   0, value 1
Chunk   0, value 2
Chunk   1, value 3
Chunk   1, value 4
Chunk   1, value 5
Chunk   2, value 6
^CTraceback (most recent call last):
  File "xxxx.py", line 15, in <module>
    for el in chunk:
  File "xxxx.py", line 2, in head
    for cnt, el in enumerate(iterable):
KeyboardInterrupt

Buuuut ... ça ne s'arrête jamais (je dois appuyer sur ^C) en raison de l while True. Je voudrais arrêter cette boucle chaque fois que le générateur a été consommé, mais je ne sais pas comment détecter cette situation. J'ai essayé de lever une exception:

class NoMoreData(Exception):
    pass

def head(iterable, max=10):
    for cnt, el in enumerate(iterable):
        yield el
        if cnt >= max:
            break
    if cnt == 0 : raise NoMoreData()

def chunks(iterable, size=10):
    i = iter(iterable)
    while True:
        try:
            yield head(i, size)
        except NoMoreData:
            break

# Sample generator: the real data is much more complex, and expensive to compute    
els = xrange(7)

for n, chunk in enumerate(chunks(els, 2)):
    for el in chunk:
        print 'Chunk %3d, value %d' % (n, el)

Mais alors l'exception n'est soulevée que dans le contexte du consommateur, ce qui n'est pas ce que je veux (je veux garder le code du consommateur propre)

Chunk   0, value 0
Chunk   0, value 1
Chunk   0, value 2
Chunk   1, value 3
Chunk   1, value 4
Chunk   1, value 5
Chunk   2, value 6
Traceback (most recent call last):
  File "xxxx.py", line 22, in <module>
    for el in chunk:
  File "xxxx.py", line 9, in head
    if cnt == 0 : raise NoMoreData
__main__.NoMoreData()

Comment puis-je détecter que le générateur est épuisé dans la fonction chunks, sans le faire marcher?

43
dangonfast

Une façon serait de jeter un œil au premier élément, le cas échéant, puis de créer et de renvoyer le générateur réel.

def head(iterable, max=10):
    first = next(iterable)      # raise exception when depleted
    def head_inner():
        yield first             # yield the extracted first element
        for cnt, el in enumerate(iterable):
            yield el
            if cnt + 1 >= max:  # cnt + 1 to include first
                break
    return head_inner()

Utilisez-le simplement dans votre générateur chunk et interceptez l'exception StopIteration comme vous l'avez fait avec votre exception personnalisée.


Mise à jour: Voici une autre version, utilisant itertools.islice pour remplacer la plupart de la fonction head et une boucle for. Cette simple boucle for fait en fait exactement la même chose comme cette lourde while-try-next-except-break construire dans le code d'origine, donc le résultat est beaucoup plus lisible.

def chunks(iterable, size=10):
    iterator = iter(iterable)
    for first in iterator:    # stops when iterator is depleted
        def chunk():          # construct generator for next chunk
            yield first       # yield element from for loop
            for more in islice(iterator, size - 1):
                yield more    # yield more elements from the iterator
        yield chunk()         # in outer generator, yield next chunk

Et nous pouvons devenir encore plus court que cela, en utilisant itertools.chain pour remplacer le générateur interne:

def chunks(iterable, size=10):
    iterator = iter(iterable)
    for first in iterator:
        yield chain([first], islice(iterator, size - 1))
61
tobias_k

Une autre façon de créer des groupes/morceaux et non la pré-promenade que le générateur utilise itertools.groupby sur une fonction clé qui utilise un itertools.count objet. Étant donné que l'objet count est indépendant de itérable , les morceaux peuvent être facilement générés sans aucune connaissance de ce que le itérable détient.

Chaque itération de groupby appelle la méthode next de l'objet count et génère une clé de groupe/bloc (suivi des éléments du bloc) en effectuant une division entière de la valeur de comptage actuelle par la taille du bloc.

from itertools import groupby, count

def chunks(iterable, size=10):
    c = count()
    for _, g in groupby(iterable, lambda _: next(c)//size):
        yield g

Chaque groupe/bloc g généré par la fonction générateur est un itérateur. Cependant, puisque groupby utilise un itérateur partagé pour tous les groupes, les itérateurs de groupe ne peuvent pas être stockés dans une liste ou un conteneur, chaque itérateur de groupe doit être consommé avant le suivant.

10
Moses Koledoye

La solution la plus rapide possible que j'ai pu trouver, grâce à (dans CPython) en utilisant des fonctions internes de niveau purement C. Ce faisant, aucun code Python octet n'est nécessaire pour produire chaque bloc (sauf si le générateur sous-jacent est implémenté en Python), ce qui présente un énorme avantage en termes de performances. Il marche chaque bloc avant de le retourner, mais il ne fait aucune pré-marche au-delà du morceau qu'il est sur le point de retourner:

# Py2 only to get generator based map
from future_builtins import map

from itertools import islice, repeat, starmap, takewhile
# operator.truth is *significantly* faster than bool for the case of
# exactly one positional argument
from operator import truth

def chunker(n, iterable):  # n is size of each chunk; last chunk may be smaller
    return takewhile(truth, map(Tuple, starmap(islice, repeat((iter(iterable), n)))))

Comme c'est un peu dense, la version étalée pour illustration:

def chunker(n, iterable):
    iterable = iter(iterable)
    while True:
        x = Tuple(islice(iterable, n))
        if not x:
            return
        yield x

Envelopper un appel à chunker dans enumerate vous permettrait de numéroter les morceaux si nécessaire.

6
ShadowRanger

Commencé à réaliser l'utilité de ce scénario lors de l'élaboration d'une solution pour l'insertion de bases de données de 500k + lignes à une vitesse plus élevée.

Un générateur traite les données de la source et les "produit" ligne par ligne; puis un autre générateur regroupe la sortie en morceaux et la "cède" par morceau. Le deuxième générateur ne connaît que la taille des morceaux et rien de plus.

Voici un exemple pour mettre en évidence le concept:

#!/usr/bin/python

def firstn_gen(n):
    num = 0
    while num < n:
        yield num
        num += 1

def chunk_gen(some_gen, chunk_size=7):
    res_chunk = []
    for count, item in enumerate(some_gen, 1):
        res_chunk.append(item)
        if count % chunk_size == 0:
            yield res_chunk
            res_chunk[:] = []
    else:
        yield res_chunk


if __== '__main__':
    for a_chunk in chunk_gen(firstn_gen(33)):
        print(a_chunk)

Testé en Python 2.7.12:

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

Que diriez-vous d'utiliser itertools.islice :

import itertools

els = iter(xrange(7))

print list(itertools.islice(els, 2))
print list(itertools.islice(els, 2))
print list(itertools.islice(els, 2))
print list(itertools.islice(els, 2))

Qui donne:

[0, 1]
[2, 3]
[4, 5]
[6]
2
warvariuc
from itertools import islice
def chunk(it, n):
    '''
    # returns chunks of n elements each

    >>> list(chunk(range(10), 3))
    [
        [0, 1, 2, ],
        [3, 4, 5, ],
        [6, 7, 8, ],
        [9, ]
    ]

    >>> list(chunk(list(range(10)), 3))
    [
        [0, 1, 2, ],
        [3, 4, 5, ],
        [6, 7, 8, ],
        [9, ]
    ]
    '''
    def _w(g):
        return lambda: Tuple(islice(g, n))
    return iter(_w(iter(it)), ())
2
igiroux

J'ai eu ce même problème, mais j'ai trouvé une solution plus simple que celles mentionnées ici:

def chunker(iterable, chunk_size):
    els = iter(iterable)
    while True:
        next_el = next(els)
        yield chain([next_el], islice(els, chunk_size - 1))

for i, chunk in enumerate(chunker(range(11), 2)):
    for el in chunk:
        print(i, el)

# Prints the following:
0 0
0 1
1 2
1 3
2 4
2 5
3 6
3 7
4 8
4 9
5 10
1
santon

Inspiré par réponse de Moses Koledoye , j'ai essayé de faire une solution qui utilise itertools.groupby mais ne nécessite pas de division à chaque étape.

La fonction suivante peut être utilisée comme clé pour groupby, et elle renvoie simplement un booléen, qui retourne après un nombre prédéfini d'appels.

def chunks(chunksize=3):

    def flag_gen():
        flag = False
        while True:
            for num in range(chunksize):
                yield flag
            flag = not flag

    flag_iter = flag_gen()

    def flag_func(*args, **kwargs):
        return next(flag_iter)

    return flag_func

Qui peut être utilisé comme ceci:

from itertools import groupby

my_long_generator = iter("abcdefghijklmnopqrstuvwxyz")

chunked_generator = groupby(my_long_generator, key=chunks(chunksize=5))

for flag, chunk in chunked_generator:
    print("Flag is {f}".format(f=flag), list(chunk))

Production:

Flag is False ['a', 'b', 'c', 'd', 'e']
Flag is True ['f', 'g', 'h', 'i', 'j']
Flag is False ['k', 'l', 'm', 'n', 'o']
Flag is True ['p', 'q', 'r', 's', 't']
Flag is False ['u', 'v', 'w', 'x', 'y']
Flag is True ['z']

J'ai fait un violon démontrant ce code .

0
Andrew Martin

Vous avez dit que vous ne souhaitez pas stocker de choses en mémoire, cela signifie-t-il que vous ne pouvez pas créer une liste intermédiaire pour le bloc actuel?

Pourquoi ne pas traverser le générateur et insérer une valeur sentinelle entre les morceaux? Le consommateur (ou un emballage approprié) pourrait ignorer la sentinelle:

class Sentinel(object):
    pass

def chunk(els, size):
    for i, el in enumerate(els):
        yield el
        if i > 0 and i % size == 0:
            yield Sentinel
0
user1961503

EDITER une autre solution avec un générateur de générateurs

Vous ne devez pas faire un while True Dans votre itérateur, mais simplement le parcourir et mettre à jour le numéro de bloc à chaque itération:

def chunk(it, maxv):
    n = 0
    for i in it:
        yield n // mavx, i
        n += 1

Si vous voulez un générateur de générateurs, vous pouvez avoir:

def chunk(a, maxv):
    def inner(it, maxv, l):
        l[0] = False
        for i in range(maxv):
            yield next(it)
        l[0] = True
        raise StopIteration
    it = iter(a)
    l = [True]
    while l[0] == True:
        yield inner(it, maxv, l)
    raise StopIteration

avec un être itérable.

Tests: sur python 2.7 et 3.4:

for i in chunk(range(7), 3):
    print 'CHUNK'
    for a in i:
        print a

donne:

CHUNK
0
1
2
CHUNK
3
4
5
CHUNK
6

Et sur 2.7:

for i in chunk(xrange(7), 3):
    print 'CHUNK'
    for a in i:
        print a

donne le même résultat.

Mais ATTENTION : list(chunk(range(7)) blocs sur 2.7 et 3.4

0
Serge Ballesta