2009-09-03 10 views
13

Comment créer un générateur répétitif, comme xrange, en Python? Par exemple, si je le fais:Comment créer un générateur répétitif en Python

>>> m = xrange(5) 
>>> print list(m) 
>>> print list(m) 

Je reçois le même résultat deux fois - les nombres 0..4. Cependant, si je tente la même chose avec le rendement:

>>> def myxrange(n): 
... i = 0 
... while i < n: 
...  yield i 
...  i += 1 
>>> m = myxrange(5) 
>>> print list(m) 
>>> print list(m) 

La deuxième fois que j'essaie de parcourir m, je reçois rien en retour - une liste vide.

Existe-t-il un moyen simple de créer un générateur répétitif comme xrange avec le rendement ou la compréhension des générateurs? J'ai trouvé a workaround on a Python tracker issue, qui utilise un décorateur pour transformer un générateur en un itérateur. Cela redémarre chaque fois que vous commencez à l'utiliser, même si vous n'avez pas utilisé toutes les valeurs la dernière fois, tout comme xrange. Je suis également venu avec mon propre décorateur, basé sur la même idée, qui retourne en fait un générateur, mais qui peut redémarrer après avoir jeté une exception StopIteration:

@decorator.decorator 
def eternal(genfunc, *args, **kwargs): 
    class _iterable: 
    iter = None 
    def __iter__(self): return self 
    def next(self, *nargs, **nkwargs): 
     self.iter = self.iter or genfunc(*args, **kwargs): 
     try: 
     return self.iter.next(*nargs, **nkwargs) 
     except StopIteration: 
     self.iter = None 
     raise 
    return _iterable() 

Est-il une meilleure façon de résoudre le problème, en utilisant seulement des compréhensions de rendement et/ou de générateur? Ou quelque chose construit en Python? Donc je n'ai pas besoin de rouler mes propres classes et décorateurs?

Mise à jour

Le comment by u0b34a0f6ae clouer la source de mon incompréhension:

xrange (5) ne retourne pas iterator, il crée un objet xrange. Les objets xrange peuvent être itérés, tout comme les dictionnaires, plus d'une fois.

Ma fonction « éternelle » aboyait entièrement le mauvais arbre, en agissant comme un itérateur/générateur (__iter__ retourne auto) plutôt que comme une collection/xrange (__iter__ retourne une nouvelle iterator).

+2

Petit pointeur, mais 'xrange()' n'est pas un générateur. 'type (xrange (4))'! = 'type (myxrange (4))'. –

+2

Je pense que c'est plus qu'un petit nitpick. C'est toute la raison de la différence. Et comme John l'a souligné, le comportement souhaité peut être obtenu avec un __iter__ surchargé. – ricree

+0

J'ai beaucoup de mal à suivre votre code d'implémentation, comparé aux deux autres implémentations proposées (l'une dans le problème du tracker Python auquel vous êtes lié, l'autre dans la réponse de @ JohnMillikin). En particulier, avoir du mal à comprendre: (1) ce que signifie exactement "@ decorator.decorator". Pouvez-vous donner un lien vers doc pour cela? (2) un exemple d'utilisation serait très utile; en particulier, celui qui exerce args et nargs (3) et pouvez-vous donner un exemple de la façon dont votre traitement StopIteration ajoute de la valeur? c'est-à-dire un exemple dans lequel votre implémentation réussit mais les deux autres implémentations échouent. –

Répondre

15

Pas directement. Une partie de la flexibilité qui permet aux générateurs d'être utilisés pour mettre en œuvre des routines, la gestion des ressources, etc., est qu'ils sont toujours uniques. Une fois exécuté, un générateur ne peut pas être réexécuté. Vous devrez créer un nouvel objet générateur.

Toutefois, vous pouvez créer votre propre classe qui remplace __iter__(). Il agira comme un générateur réutilisable:

def multigen(gen_func): 
    class _multigen(object): 
     def __init__(self, *args, **kwargs): 
      self.__args = args 
      self.__kwargs = kwargs 
     def __iter__(self): 
      return gen_func(*self.__args, **self.__kwargs) 
    return _multigen 

@multigen 
def myxrange(n): 
    i = 0 
    while i < n: 
    yield i 
    i += 1 
m = myxrange(5) 
print list(m) 
print list(m) 
+0

C'est fondamentalement identique à la solution de contournement à laquelle j'ai fait référence dans ma question - je suppose que vous l'avez manqué. Mais merci! –

+2

"Solution de contournement". Il n'y en a pas beaucoup. xrange (5) ne renvoie pas d'itérateur, il crée un objet xrange. Les objets xrange peuvent être itérés, tout comme les dictionnaires, plus d'une fois. – u0b34a0f6ae

0

Je pense que la réponse à cette question est "Non". J'ai peut-être tort. C'est peut-être le cas avec certaines des nouvelles choses géniales que vous pouvez faire avec les générateurs dans 2.6 impliquant des arguments et la gestion des exceptions qui permettrait quelque chose comme ce que vous voulez. Mais ces fonctionnalités sont principalement destinées à la mise en œuvre de semi-continuations. Pourquoi voulez-vous ne pas avoir vos propres classes ou décorateurs? Et pourquoi vouliez-vous créer un décorateur qui renvoyait un générateur au lieu d'une instance de classe?

+0

(1) Parce qu'il faut plus de code, et semble être quelque chose qui aurait pu être implémenté d'une manière que je ne connaissais pas. (2) Parce que j'ai d'abord mal compris xrange, et j'ai pensé que je voulais un générateur éternel plutôt qu'un itératif. –

1

Si vous écrivez beaucoup de ceux-ci, la réponse de John Millikin est la plus propre qu'il obtient.

Mais si cela ne vous dérange pas d'ajouter 3 lignes et un peu d'indentation, vous pouvez le faire sans un décorateur personnalisé.Ce compose de 2 tours:

  1. [Généralement utile:] Vous pouvez facilement faire une classe itérables sans mettre en œuvre .next() - il suffit d'utiliser un générateur pour __iter__(self)!

  2. Au lieu de vous préoccuper d'un constructeur, vous pouvez définir une classe unique dans une fonction.

=>

def myxrange(n): 
    class Iterable(object): 
     def __iter__(self): 
      i = 0 
      while i < n: 
       yield i 
       i += 1 
    return Iterable() 

petits caractères: Je n'ai pas testé les performances, les classes de frai comme cela pourrait être un gaspillage. Mais génial ;-)

-1

utiliser cette solution:

>>> myxrange_ = lambda x: myxrange(x) 
>>> print list(myxrange_(5)) 
... [0, 1, 2, 3, 4] 
>>> print list(myxrange_(5)) 
... [0, 1, 2, 3, 4] 

>>> for number in myxrange_(5): 
...  print number 
... 
    0 
    1 
    2 
    3 
    4 
>>> 

et avec un décorateur:

>>> def decorator(generator): 
...  return lambda x: generator(x) 
... 
>>> @decorator 
>>> def myxrange(n): 
... i = 0 
... while i < n: 
...  yield i 
...  i += 1 
... 
>>> print list(myxrange(5)) 
... [0, 1, 2, 3, 4] 
>>> print list(myxrange(5)) 
... [0, 1, 2, 3, 4] 
>>> 

simple.

+0

¿-1 ?, ne comprends pas cela. cette solution répond aux exigences de la question. – SmartElectron

+0

Non, car les paramètres nécessaires pour répéter l'itération ne sont pas encapsulés dans l'objet - ils doivent être transmis chaque fois qu'une nouvelle réutilisation est souhaitée. Le point entier d'un objet générateur est d'encapsuler les informations nécessaires pour effectuer l'itération. Un générateur resuable devrait également faire cela.Notez que la solution de John Millikin ne vous oblige pas à taper "5" chaque fois que vous voulez utiliser le générateur. Vous tapez "5" une fois et l'objet se souvient pour vous à partir de ce point. – Paul

+0

En outre, la syntaxe n'est pas la même que pour un générateur. Des parenthèses supplémentaires sont nécessaires. – Paul

1

En utilisant itertools c'est super facile.

import itertools 

alist = [1,2,3] 
repeatingGenerator = itertools.cycle(alist) 

print(next(generatorInstance)) #=> yields 1 
print(next(generatorInstance)) #=> yields 2 
print(next(generatorInstance)) #=> yields 3 
print(next(generatorInstance)) #=> yields 1 again! 
+0

Cela provoquerait 'print list (m)' (une exigence dans la question) pour s'exécuter pour toujours, cependant: / –

0

Vous pouvez réinitialiser itérateurs avec more_itertools.seekable, un outil tiers.

Installation via > pip install more_itertools.

import more_itertools as mit 


def myxrange(n): 
    """Yield integers.""" 
    i = 0 
    while i < n: 
     yield i 
     i += 1 

m = mit.seekable(myxrange(5)) 
print(list(m)) 
m.seek(0)            # reset iterator 
print(list(m)) 
# [0, 1, 2, 3, 4] 
# [0, 1, 2, 3, 4] 

Note: la consommation de mémoire se développe tout en faisant avancer un itérateur, afin de se méfier emballage iterables grandes.

Questions connexes