web-dev-qa-db-fra.com

Réinitialisation de l'objet générateur en Python

J'ai un objet générateur retourné par plusieurs rendements. La préparation pour appeler ce générateur est une opération plutôt fastidieuse. C'est pourquoi je veux 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 pense à copier le contenu dans une simple liste. 

120
Dewfy

Une autre option consiste à utiliser la fonction itertools.tee() 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 pourrait être avantageux du point de vue de l'utilisation de la mémoire si l'itération d'origine ne traitait pas tous les éléments.

93
Ants Aasma

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

  1. Exécutez à nouveau la fonction de 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 sur laquelle vous pouvez effectuer une nouvelle itération:

    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'il calcule à nouveau les valeurs. Si cela nécessite beaucoup de temps, 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 dans la mémoire. S'il y a trop de valeurs, cela peut s'avérer peu pratique.

Donc, vous avez le compromis classique mémoire vs traitement. Je ne peux imaginer une façon de rembobiner le générateur sans stocker les valeurs ni les calculer à nouveau.

118
nosklo
>>> 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
28
aaab

La solution la plus simple est probablement d’envelopper la partie coûteuse dans un objet et de 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 RAM en même temps, utilisez list() pour matérialiser les résultats du générateur dans une liste simple et les utiliser.

24
Aaron Digulla

Je veux offrir une solution différente à un vieux 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 ceci par rapport à quelque chose comme list(iterator) est qu'il s'agit de O(1) espace complexe et list(iterator) est O(n). L'inconvénient est que, si vous avez uniquement accès à l'itérateur, mais pas à la fonction qui l'a généré, vous ne pouvez pas utiliser cette méthode. Par exemple, il peut sembler raisonnable de procéder comme 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)
13
michaelsnowden

Si la réponse de GrzegorzOledzki ne suffit pas, vous pouvez probablement 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.

UPDATE: Voir aussi itertools.tee() . Cela implique une partie de ce compromis mémoire/traitement mentionné ci-dessus, mais cela pourrait économiser de la mémoire par rapport au simple stockage du générateur, générant un list ; cela dépend de la façon dont vous utilisez le générateur.

5
Hank Gay

Si votre générateur est pur en ce sens que sa sortie dépend uniquement 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 s'avérer 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
Ben Usman

De documentation officielle de tee :

En général, si un itérateur utilise la plupart ou la totalité des données avant un autre itérateur commence, il est plus rapide d’utiliser list () au lieu de tee ().

Il est donc préférable d’utiliser plutôt list(iterable) dans votre cas.

3
Shubham Chaudhary

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

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

  return FunctionWithYield

Maintenant, vous pouvez faire autant de fois que vous le souhaitez:

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

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 avec l'avancement de l'itérateur. Méfiez-vous des gros iterables. 

1
pylang

Il n'y a pas d'option pour réinitialiser les itérateurs. L'itérateur apparaît généralement lorsqu'il se répète via la fonction next(). Le seul moyen consiste à effectuer une sauvegarde avant itération 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 dans 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'article 0 est déjà sorti. De plus, tous les éléments apparaissent 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 avec iter(<list-object>).

1
Amalraj Victory

Utilisation d'une fonction wrapper pour gérer StopIteration

Vous pouvez écrire une fonction wrapper simple dans votre fonction génératrice qui permet de suivre le moment où le générateur est épuisé. Il utilisera l'exception StopIteration qu'un générateur lève 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 constater ci-dessus, lorsque notre fonction wrapper intercepte une exception StopIteration, elle réinitialise simplement l'objet générateur (en utilisant une autre instance de l'appel de fonction).

Et ensuite, en supposant que vous définissiez votre fonction fournissant un générateur comme ci-dessous, vous pourriez utiliser la syntaxe du décorateur de fonctions Python pour l'envelopper de manière implicite:

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

Je ne suis pas sûr de ce que vous entendiez par préparation coûteuse, mais je suppose que vous avez

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?

0
ilya n.

Ok, vous dites que vous voulez appeler un générateur plusieurs fois, mais l'initialisation est coûteuse ... Et si 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

Vous pouvez également créer votre propre classe qui suit le protocole 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-typeshttp://anandology.com/python-practice-book/iterators.html

0
tvt173