Réinitialiser l'objet générateur en Python


153

J'ai un objet générateur renvoyé par rendement multiple. La préparation pour appeler ce générateur est une opération plutôt longue. C'est pourquoi je souhaite réutiliser le générateur plusieurs fois.

y = FunctionWithYield()
for x in y: print(x)
#here must be something to reset 'y'
for x in y: print(x)

Bien sûr, je prends en compte la copie du contenu dans une liste simple. Existe-t-il un moyen de réinitialiser mon générateur?

Réponses:


119

Une autre option consiste à utiliser la itertools.tee()fonction pour créer une deuxième version de votre générateur:

y = FunctionWithYield()
y, y_backup = tee(y)
for x in y:
    print(x)
for x in y_backup:
    print(x)

Cela peut être avantageux du point de vue de l'utilisation de la mémoire si l'itération d'origine ne traite pas tous les éléments.


33
Si vous vous demandez ce que cela va faire dans ce cas, il s'agit essentiellement de mettre en cache des éléments dans la liste. Vous pouvez donc aussi bien l'utiliser y = list(y)avec le reste de votre code inchangé.
ilya n.

5
tee () créera une liste en interne pour stocker les données, c'est donc la même chose que j'ai fait dans ma réponse.
nosklo

6
Regardez l'implémentation ( docs.python.org/library/itertools.html#itertools.tee ) - cela utilise une stratégie de chargement différé , donc les éléments à lister copiés uniquement à la demande
Dewfy

11
@Dewfy: Ce qui sera plus lent puisque tous les éléments devront de toute façon être copiés.
nosklo

8
oui, list () est mieux dans ce cas. tee n'est utile que si vous ne consommez pas toute la liste
gravitation

148

Les générateurs ne peuvent pas être rembobinés. Vous avez les options suivantes:

  1. Exécutez à nouveau la fonction générateur en redémarrant la génération:

    y = FunctionWithYield()
    for x in y: print(x)
    y = FunctionWithYield()
    for x in y: print(x)
  2. Stockez les résultats du générateur dans une structure de données sur la mémoire ou le disque que vous pouvez répéter:

    y = list(FunctionWithYield())
    for x in y: print(x)
    # can iterate again:
    for x in y: print(x)

L'inconvénient de l'option 1 est qu'elle calcule à nouveau les valeurs. Si cela demande beaucoup de ressources processeur, vous finissez par calculer deux fois. D'autre part, l'inconvénient de 2 est le stockage. La liste complète des valeurs sera stockée en mémoire. S'il y a trop de valeurs, cela peut ne pas être pratique.

Vous avez donc le compromis classique entre la mémoire et le traitement . Je ne peux pas imaginer un moyen de rembobiner le générateur sans stocker les valeurs ni les calculer à nouveau.


Peut-être existe-t-il un moyen de sauvegarder la signature de l'appel de fonction? FunctionWithYield, param1, param2 ...
Dewfy

3
@Dewfy: sure: def call_my_func (): return FunctionWithYield (param1, param2)
nosklo

@Dewfy Qu'entendez-vous par "enregistrer la signature de l'appel de fonction"? Pouvez-vous expliquer? Voulez-vous dire sauvegarder les paramètres transmis au générateur?
Андрей Беньковский

2
Un autre inconvénient de (1) est également que FunctionWithYield () peut être non seulement coûteux, mais impossible à recalculer, par exemple s'il lit depuis stdin.
Max

2
Pour faire écho à ce que @Max a dit, si la sortie de la fonction peut (ou va) changer entre les appels, (1) peut donner des résultats inattendus et / ou indésirables.
Sam_Butler

36
>>> def gen():
...     def init():
...         return 0
...     i = init()
...     while True:
...         val = (yield i)
...         if val=='restart':
...             i = init()
...         else:
...             i += 1

>>> g = gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
>>> g.send('restart')
0
>>> g.next()
1
>>> g.next()
2

29

La solution la plus simple consiste probablement à envelopper la pièce coûteuse dans un objet et à la transmettre au générateur:

data = ExpensiveSetup()
for x in FunctionWithYield(data): pass
for x in FunctionWithYield(data): pass

De cette façon, vous pouvez mettre en cache les calculs coûteux.

Si vous pouvez conserver tous les résultats dans la RAM en même temps, utilisez list()pour matérialiser les résultats du générateur dans une liste simple et travaillez avec cela.


23

Je souhaite proposer une solution différente à un ancien problème

class IterableAdapter:
    def __init__(self, iterator_factory):
        self.iterator_factory = iterator_factory

    def __iter__(self):
        return self.iterator_factory()

squares = IterableAdapter(lambda: (x * x for x in range(5)))

for x in squares: print(x)
for x in squares: print(x)

L'avantage de cela par rapport à quelque chose comme list(iterator)c'est que c'est O(1)la complexité de l'espace et l' list(iterator)est O(n). L'inconvénient est que si vous avez uniquement accès à l'itérateur, mais pas à la fonction qui a produit l'itérateur, vous ne pouvez pas utiliser cette méthode. Par exemple, il peut sembler raisonnable de faire ce qui suit, mais cela ne fonctionnera pas.

g = (x * x for x in range(5))

squares = IterableAdapter(lambda: g)

for x in squares: print(x)
for x in squares: print(x)

@Dewfy Dans le premier extrait, le générateur est sur la ligne "squares = ...". Les expressions de générateur se comportent de la même manière que l'appel d'une fonction qui utilise yield, et je n'en ai utilisé qu'une car c'est moins verbeux que d'écrire une fonction avec yield pour un exemple aussi court. Dans le deuxième extrait, j'ai utilisé FunctionWithYield comme générateur_factory, donc il sera appelé chaque fois que iter est appelé, c'est-à-dire chaque fois que j'écris "for x in y".
michaelsnowden

Bonne solution. Cela rend en fait un objet itérable sans état au lieu d'un objet itérateur avec état, de sorte que l'objet lui-même est réutilisable. Particulièrement utile si vous souhaitez passer un objet itérable à une fonction et que cette fonction utilisera l'objet plusieurs fois.
Cosyn

5

Si la réponse de GrzegorzOledzki ne suffit pas, vous pourriez probablement l'utiliser send()pour atteindre votre objectif. Voir PEP-0342 pour plus de détails sur les générateurs améliorés et les expressions de rendement.

MISE À JOUR: voir également itertools.tee(). Cela implique une partie de ce compromis entre la mémoire et le traitement mentionné ci-dessus, mais cela pourrait économiser de la mémoire en stockant simplement les résultats du générateur dans un list; cela dépend de la manière dont vous utilisez le générateur.


5

Si votre générateur est pur dans le sens où sa sortie ne dépend que des arguments passés et du numéro d'étape, et que vous voulez que le générateur résultant puisse être redémarré, voici un extrait de tri qui pourrait être utile:

import copy

def generator(i):
    yield from range(i)

g = generator(10)
print(list(g))
print(list(g))

class GeneratorRestartHandler(object):
    def __init__(self, gen_func, argv, kwargv):
        self.gen_func = gen_func
        self.argv = copy.copy(argv)
        self.kwargv = copy.copy(kwargv)
        self.local_copy = iter(self)

    def __iter__(self):
        return self.gen_func(*self.argv, **self.kwargv)

    def __next__(self):
        return next(self.local_copy)

def restartable(g_func: callable) -> callable:
    def tmp(*argv, **kwargv):
        return GeneratorRestartHandler(g_func, argv, kwargv)

    return tmp

@restartable
def generator2(i):
    yield from range(i)

g = generator2(10)
print(next(g))
print(list(g))
print(list(g))
print(next(g))

les sorties:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
0
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1

3

De la documentation officielle du tee :

En général, si un itérateur utilise la plupart ou toutes les données avant qu'un autre itérateur ne démarre, il est plus rapide d'utiliser list () au lieu de tee ().

Il est donc préférable de l'utiliser à la list(iterable)place dans votre cas.


6
qu'en est-il des générateurs infinis?
Dewfy

1
La vitesse n'est pas la seule considération; list()met tout l'itérable en mémoire
Chris_Rands

@Chris_Rands Il en sera de même tee()si un itérateur consomme toutes les valeurs - c'est comme ça que ça teemarche.
AChampion le

2
@Dewfy: pour les générateurs infinis, utilisez la solution d'Aaron Digulla (fonction ExpensiveSetup renvoyant les précieuses données.)
Jeff Learman

3

Utilisation d'une fonction wrapper pour gérer StopIteration

Vous pouvez écrire une simple fonction wrapper dans votre fonction de génération de générateur qui suit lorsque le générateur est épuisé. Il le fera en utilisant l' StopIterationexception qu'un générateur lance lorsqu'il atteint la fin de l'itération.

import types

def generator_wrapper(function=None, **kwargs):
    assert function is not None, "Please supply a function"
    def inner_func(function=function, **kwargs):
        generator = function(**kwargs)
        assert isinstance(generator, types.GeneratorType), "Invalid function"
        try:
            yield next(generator)
        except StopIteration:
            generator = function(**kwargs)
            yield next(generator)
    return inner_func

Comme vous pouvez le voir ci-dessus, lorsque notre fonction wrapper intercepte une StopIterationexception, elle réinitialise simplement l'objet générateur (en utilisant une autre instance de l'appel de fonction).

Et puis, en supposant que vous définissiez votre fonction de fourniture de générateur quelque part comme ci-dessous, vous pouvez utiliser la syntaxe du décorateur de fonction Python pour l'envelopper implicitement:

@generator_wrapper
def generator_generating_function(**kwargs):
    for item in ["a value", "another value"]
        yield item

2

Vous pouvez définir une fonction qui renvoie votre générateur

def f():
  def FunctionWithYield(generator_args):
    code here...

  return FunctionWithYield

Vous pouvez maintenant faire autant de fois que vous le souhaitez:

for x in f()(generator_args): print(x)
for x in f()(generator_args): print(x)

1
Merci pour la réponse, mais le point principal de la question était d'éviter la création , l'invocation de la fonction intérieure ne fait que cacher la création - vous la créez deux fois
Dewfy

1

Je ne sais pas ce que vous entendez par préparation coûteuse, mais je suppose que vous avez en fait

data = ... # Expensive computation
y = FunctionWithYield(data)
for x in y: print(x)
#here must be something to reset 'y'
# this is expensive - data = ... # Expensive computation
# y = FunctionWithYield(data)
for x in y: print(x)

Si tel est le cas, pourquoi ne pas réutiliser data?


1

Il n'y a pas d'option pour réinitialiser les itérateurs. Iterator apparaît généralement lorsqu'il itère dans la next()fonction. Le seul moyen est de faire une sauvegarde avant d'itérer sur l'objet itérateur. Vérifiez ci-dessous.

Création d'un objet itérateur avec les éléments 0 à 9

i=iter(range(10))

Itération à travers la fonction next () qui apparaîtra

print(next(i))

Conversion de l'objet itérateur en liste

L=list(i)
print(L)
output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

donc l'élément 0 est déjà sorti. De plus, tous les éléments sont affichés lorsque nous avons converti l'itérateur en liste.

next(L) 

Traceback (most recent call last):
  File "<pyshell#129>", line 1, in <module>
    next(L)
StopIteration

Vous devez donc convertir l'itérateur en listes pour la sauvegarde avant de commencer l'itération. La liste peut être convertie en itérateur aveciter(<list-object>)


1

Vous pouvez maintenant utiliser more_itertools.seekable(un outil tiers) qui permet de réinitialiser les itérateurs.

Installer via > pip install more_itertools

import more_itertools as mit


y = mit.seekable(FunctionWithYield())
for x in y:
    print(x)

y.seek(0)                                              # reset iterator
for x in y:
    print(x)

Remarque: la consommation de mémoire augmente lors de l'avancement de l'itérateur, alors méfiez-vous des grands itérables.


1

Vous pouvez le faire en utilisant itertools.cycle (), vous pouvez créer un itérateur avec cette méthode, puis exécuter une boucle for sur l'itérateur qui bouclera sur ses valeurs.

Par exemple:

def generator():
for j in cycle([i for i in range(5)]):
    yield j

gen = generator()
for i in range(20):
    print(next(gen))

générera 20 nombres, de 0 à 4 à plusieurs reprises.

Une note de la documentation:

Note, this member of the toolkit may require significant auxiliary storage (depending on the length of the iterable).

+1 parce que cela fonctionne, mais je vois 2 problèmes là-bas 1) une grande empreinte mémoire puisque la documentation indique "créer une copie" 2) Une boucle infinie n'est certainement pas ce que je veux
Dewfy

0

Ok, vous dites que vous voulez appeler un générateur plusieurs fois, mais l'initialisation coûte cher ... Et quelque chose comme ça?

class InitializedFunctionWithYield(object):
    def __init__(self):
        # do expensive initialization
        self.start = 5

    def __call__(self, *args, **kwargs):
        # do cheap iteration
        for i in xrange(5):
            yield self.start + i

y = InitializedFunctionWithYield()

for x in y():
    print x

for x in y():
    print x

Alternativement, vous pouvez simplement créer votre propre classe qui suit le protocole de l'itérateur et définit une sorte de fonction de «réinitialisation».

class MyIterator(object):
    def __init__(self):
        self.reset()

    def reset(self):
        self.i = 5

    def __iter__(self):
        return self

    def next(self):
        i = self.i
        if i > 0:
            self.i -= 1
            return i
        else:
            raise StopIteration()

my_iterator = MyIterator()

for x in my_iterator:
    print x

print 'resetting...'
my_iterator.reset()

for x in my_iterator:
    print x

https://docs.python.org/2/library/stdtypes.html#iterator-types http://anandology.com/python-practice-book/iterators.html


Vous déléguez simplement le problème au wrapper. Supposons qu'une initialisation coûteuse crée un générateur. Ma question portait sur la réinitialisation de votre__call__
Dewfy

Ajout d'un deuxième exemple en réponse à votre commentaire. Il s'agit essentiellement d'un générateur personnalisé avec une méthode de réinitialisation.
tvt173

0

Ma réponse résout un problème légèrement différent: si le générateur est coûteux à initialiser et que chaque objet généré est coûteux à générer. Mais nous devons consommer le générateur plusieurs fois dans plusieurs fonctions. Afin d'appeler le générateur et chaque objet généré exactement une fois, nous pouvons utiliser des threads et exécuter chacune des méthodes consommatrices dans différents threads. Il se peut que nous n'atteignions pas le vrai parallélisme en raison du GIL, mais nous atteindrons notre objectif.

Cette approche a fait du bon travail dans le cas suivant: le modèle d'apprentissage en profondeur traite beaucoup d'images. Le résultat est beaucoup de masques pour un grand nombre d'objets sur l'image. Chaque masque consomme de la mémoire. Nous avons environ 10 méthodes qui font des statistiques et des métriques différentes, mais elles prennent toutes les images à la fois. Toutes les images ne peuvent pas tenir en mémoire. Les méthodes peuvent facilement être réécrites pour accepter l'itérateur.

class GeneratorSplitter:
'''
Split a generator object into multiple generators which will be sincronised. Each call to each of the sub generators will cause only one call in the input generator. This way multiple methods on threads can iterate the input generator , and the generator will cycled only once.
'''

def __init__(self, gen):
    self.gen = gen
    self.consumers: List[GeneratorSplitter.InnerGen] = []
    self.thread: threading.Thread = None
    self.value = None
    self.finished = False
    self.exception = None

def GetConsumer(self):
    # Returns a generator object. 
    cons = self.InnerGen(self)
    self.consumers.append(cons)
    return cons

def _Work(self):
    try:
        for d in self.gen:
            for cons in self.consumers:
                cons.consumed.wait()
                cons.consumed.clear()

            self.value = d

            for cons in self.consumers:
                cons.readyToRead.set()

        for cons in self.consumers:
            cons.consumed.wait()

        self.finished = True

        for cons in self.consumers:
            cons.readyToRead.set()
    except Exception as ex:
        self.exception = ex
        for cons in self.consumers:
            cons.readyToRead.set()

def Start(self):
    self.thread = threading.Thread(target=self._Work)
    self.thread.start()

class InnerGen:
    def __init__(self, parent: "GeneratorSplitter"):
        self.parent: "GeneratorSplitter" = parent
        self.readyToRead: threading.Event = threading.Event()
        self.consumed: threading.Event = threading.Event()
        self.consumed.set()

    def __iter__(self):
        return self

    def __next__(self):
        self.readyToRead.wait()
        self.readyToRead.clear()
        if self.parent.finished:
            raise StopIteration()
        if self.parent.exception:
            raise self.parent.exception
        val = self.parent.value
        self.consumed.set()
        return val

Ussage:

genSplitter = GeneratorSplitter(expensiveGenerator)

metrics={}
executor = ThreadPoolExecutor(max_workers=3)
f1 = executor.submit(mean,genSplitter.GetConsumer())
f2 = executor.submit(max,genSplitter.GetConsumer())
f3 = executor.submit(someFancyMetric,genSplitter.GetConsumer())
genSplitter.Start()

metrics.update(f1.result())
metrics.update(f2.result())
metrics.update(f3.result())

Vous venez de réinventer itertools.isliceou pour asynchrone aiostream.stream.take, et cet article vous permet de le faire de manière asynchrone / d'attente stackoverflow.com/a/42379188/149818
Dewfy

-3

Cela peut être fait par objet code. Voici l'exemple.

code_str="y=(a for a in [1,2,3,4])"
code1=compile(code_str,'<string>','single')
exec(code1)
for i in y: print i

1 2 3 4

for i in y: print i


exec(code1)
for i in y: print i

1 2 3 4


4
eh bien, il fallait en fait réinitialiser le générateur pour éviter d'exécuter deux fois le code d'initialisation. Votre approche (1) exécute l'initialisation deux fois de toute façon, (2) elle implique execcela légèrement non recommandé pour un cas aussi simple.
Dewfy
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.