50

I'd like a function, is_just_started, which behaves like the following:

>>> def gen(): yield 0; yield 1
>>> a = gen()
>>> is_just_started(a) 
True
>>> next(a)
0
>>> is_just_started(a) 
False
>>> next(a)
1
>>> is_just_started(a) 
False
>>> next(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> is_just_started(a)
False

How can I implement this function?

I looked at the .gi_running attribute but it appears to be used for something else.

If I know the first value that needs to be sent into the generator, I can do something like this:

def safe_send(gen, a):
    try:
        return gen.send(a)
    except TypeError as e:
        if "just-started" in e.args[0]:
            gen.send(None)
            return gen.send(a)
        else:
            raise

However, this seems abhorrent.

Zombo
  • 1
  • 62
  • 391
  • 407
Claudiu
  • 224,032
  • 165
  • 485
  • 680
  • 3
    Is it allowed to modify inside the generator itself? Is it allowed to decorate it? – wim Dec 23 '16 at 19:43
  • 4
    it looks like `gi_running` indicates that the interpreter is actually currently running the code, so it's false between iterations – Tim Tisdall Dec 23 '16 at 20:18
  • Possible duplicate of [Is there a Python function that checks if a generator is started?](http://stackoverflow.com/questions/17684908/is-there-a-python-function-that-checks-if-a-generator-is-started) – Chris_Rands Mar 10 '17 at 10:49

3 Answers3

70

This only works in Python 3.2+:

>>> def gen(): yield 0; yield 1
... 
>>> a = gen()
>>> import inspect
>>> inspect.getgeneratorstate(a)
'GEN_CREATED'
>>> next(a)
0
>>> inspect.getgeneratorstate(a)
'GEN_SUSPENDED'
>>> next(a)
1
>>> inspect.getgeneratorstate(a)
'GEN_SUSPENDED'
>>> next(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> inspect.getgeneratorstate(a)
'GEN_CLOSED'

So, the requested function is:

import inspect

def is_just_started(gen):
    return inspect.getgeneratorstate(gen) == inspect.GEN_CREATED:

Out of curiosity, I looked into CPython to figure out how it was determining this... Apparently it looks at generator.gi_frame.f_lasti which is the "index of last attempted instruction in bytecode". If it's -1 then it hasn't started yet.

Here's a py2 version:

def is_just_started(gen):
    return gen.gi_frame is not None and gen.gi_frame.f_lasti == -1
Dunes
  • 37,291
  • 7
  • 81
  • 97
Tim Tisdall
  • 9,914
  • 3
  • 52
  • 82
  • 13
    This is why I love stackoverflow. Even better, it looks like this is [well-defined](https://docs.python.org/3.2/library/inspect.html#inspect.getgeneratorstate). – Claudiu Dec 23 '16 at 20:02
  • What is `GEN_RUNNING` good for? the doc says"if it is currently being executed by the interpreter", but I thought that's nonsensical because of GIL? – cat Dec 23 '16 at 23:09
  • 1
    @cat Phrasing is a bit weird. The generator could be running but blocked by IO (e.g. [this gist](https://gist.github.com/earwig/c6277887f872ffd1785f9a67fc629d7c)). I suspect it will also happen if the generator is in the middle of execution but deferred by the scheduler. – Ben Kurtovic Dec 23 '16 at 23:59
  • 1
    @cat: Maybe you're *in* the generator. It'll also be the state of generators running in other threads, whether or not those threads are blocked by the GIL. – user2357112 Dec 24 '16 at 00:24
  • 3
    @cat: `def g(): yield inspect.getgeneratorstate(x)` `x = g(); next(x)` – deltab Dec 24 '16 at 00:41
  • Is this guaranteed outside of CPython - for instance PYPY or other Python implementations - or does this boil down to CPython implementation detail. – Tony Suffolk 66 Dec 27 '16 at 10:31
  • @TonySuffolk66 - I don't know. The 3.2+ version of the method is in the documentation of `inspect` so it should work in pypy3 and other implementations that support 3.2+ (otherwise it should be considered a bug in their implementation). The py2 version makes use of something I found in the CPython source and I'm not sure if it's documented as being part of the language (you'd have to search the PEP's to see if it's defined there). – Tim Tisdall Dec 27 '16 at 13:37
26

Make a new generator which simply yields from your generator of interest. It sets a flag once the first value has been consumed. Afterwards, it can simply use yield from for the rest of the items.

Use the substitute generator as a drop in replacement for the generator you're interested in monitoring the "is_just_started" state.

This technique is non-intrusive, and can be used even on generators for which you have no control over the source code.

wim
  • 338,267
  • 99
  • 616
  • 750
5

You may create a iterator and set the flag as the instance property to iterator class as:

class gen(object):
    def __init__(self, n):
        self.n = n
        self.num, self.nums = 0, []
        self.is_just_started = True  # Your flag

    def __iter__(self):
        return self

    # Python 3 compatibility
    def __next__(self):
        return self.next()

    def next(self):
        self.is_just_started = False  # Reset flag with next
        if self.num < self.n:
            cur, self.num = self.num, self.num+1
            return cur
        else:
            raise StopIteration()

And your value check function would be like:

def is_just_started(my_generator):
    return my_generator.is_just_started

Sample run:

>>> a = gen(2)

>>> is_just_started(a)
True

>>> next(a)
0
>>> is_just_started(a)
False

>>> next(a)
1
>>> is_just_started(a)
False

>>> next(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 19, in next
StopIteration

To know the difference between iterator and generator, check Difference between Python's Generators and Iterators

Community
  • 1
  • 1
Moinuddin Quadri
  • 46,825
  • 13
  • 96
  • 126