(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:
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?
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))
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.
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.
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]
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]
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)), ())
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
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 .
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
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