Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make a repeating generator in Python

Tags:

python

How do you make a repeating generator, like xrange, in Python? For instance, if I do:

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

I get the same result both times — the numbers 0..4. However, if I try the same with yield:

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

The second time I try to iterate over m, I get nothing back — an empty list.

Is there a simple way to create a repeating generator like xrange with yield, or generator comprehensions? I found a workaround on a Python tracker issue, which uses a decorator to transform a generator into an iterator. This restarts every time you start using it, even if you didn't use all the values last time through, just like xrange. I also came up with my own decorator, based on the same idea, which actually returns a generator, but one which can restart after throwing a StopIteration exception:

@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()

Is there a better way to solve the problem, using only yield and/or generator comprehensions? Or something built into Python? So I don't need to roll my own classes and decorators?

Update

The comment by u0b34a0f6ae nailed the source of my misunderstanding:

xrange(5) does not return an iterator, it creates an xrange object. xrange objects can be iterated, just like dictionaries, more than once.

My "eternal" function was barking up the wrong tree entirely, by acting like an iterator/generator (__iter__ returns self) rather than like a collection/xrange (__iter__ returns a new iterator).

like image 998
Alice Purcell Avatar asked Sep 03 '09 23:09

Alice Purcell


People also ask

How do you create a generator in Python?

Create Generators in Python It is fairly simple to create a generator in Python. It is as easy as defining a normal function, but with a yield statement instead of a return statement. If a function contains at least one yield statement (it may contain other yield or return statements), it becomes a generator function.

Can a generator be called multiple times in Python?

Yes, generator can be used only once.

Is generator faster than for loop Python?

This generator uses an iterator, because the "for" loop is implemented using an iterator. If you time these, the generator is consistently faster.

Why would you use a generator over a loop in Python?

A generator is a construct in Python that allows for lazy or ad hoc loading of a stream of data. They can work like a list and be looped over, but generators have the ability to maintain state. Looking at the function above, you might be seeing an unfamiliar keyword called yield . This is similar to return .


3 Answers

Not directly. Part of the flexibility that allows generators to be used for implementing co-routines, resource management, etc, is that they are always one-shot. Once run, a generator cannot be re-run. You would have to create a new generator object.

However, you can create your own class which overrides __iter__(). It will act like a reusable generator:

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)
like image 174
John Millikin Avatar answered Oct 18 '22 08:10

John Millikin


Using itertools its super easy.

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!
like image 40
Matt S Avatar answered Oct 18 '22 08:10

Matt S


If you write a lot of these, John Millikin's answer is the cleanest it gets.

But if you don't mind adding 3 lines and some indentation, you can do it without a custom decorator. This composes 2 tricks:

  1. [Generally useful:] You can easily make a class iterable without implementing .next() - just use a generator for __iter__(self)!

  2. Instead of bothering with a constructor, you can define a one-off class inside a function.

=>

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

Small print: I didn't test performance, spawning classes like this might be wasteful. But awesome ;-)

like image 37
Beni Cherniavsky-Paskin Avatar answered Oct 18 '22 10:10

Beni Cherniavsky-Paskin