Holger
Holger

Reputation: 2175

Is there a Python function that checks if a generator is started?

I try to define a generator function mycount() that can be reset with the generator function send(0) as in the example below. Everything works fine, except when I use send(0) on a new generator object that hasn't started yet. In this case it gives a TypeError. Is there any function that checks if the generator has started or do I have to catch the TypeError and create a new generator object with mycount(0) in such case?

def mycount(value):
    while True:
        v = yield value
        if v == None:
            value = value + 1
        else:
            value = v

g = mycount(3)
print(next(g))    # prints 3
print(next(g))    # prints 4
print(g.send(0))  # prints 0
print(next(g))    # prints 1
print(next(g))    # prints 2

g2 = mycount(3)
g2.send(0)
# TypeError: can't send non-None value to a just-started generator

Upvotes: 8

Views: 2424

Answers (4)

elkurto
elkurto

Reputation: 11

Here's a complete implementation Python2 compatible routine, getgeneratorstate(gtor), with test code.

import unittest
import enum

class GtorState(enum.Enum):
    GEN_RUNNING ='GEN_RUNNING'
    GEN_CLOSED ='GEN_CLOSED'
    GEN_CREATED ='GEN_CREATED'
    GEN_SUSPENDED ='GEN_SUSPENDED'

    @staticmethod
    def getgeneratorstate(gtor):
        if gtor.gi_running:
            return GtorState.GEN_RUNNING

        if gtor.gi_frame is None:
            return GtorState.GEN_CLOSED

        if gtor.gi_frame.f_lasti == -1:
            return GtorState.GEN_CREATED

        return GtorState.GEN_SUSPENDED
    #end-def

def coro000():
    """ a coroutine that does little 

    """ 
    print('-> coroutine started')
    x =yield
    print('-> coroutine received ', x)


class Test_Coro(unittest.TestCase):
    def test_coro000(self):

        my_coro000 =coro000()
        self.assertEqual( GtorState.getgeneratorstate(my_coro000), GtorState.GEN_CREATED)

        next(my_coro000)  # prints '-> coroutine started'
        self.assertEqual( GtorState.getgeneratorstate(my_coro000), GtorState.GEN_SUSPENDED)

        try:
            my_coro000.send(42)  # prints '-> coroutine received 42 
            self.assertEqual( GtorState.getgeneratorstate(my_coro000), GtorState.GEN_SUSPENDED)

            self.fail('should have raised StopIteration ')

        except StopIteration:
            self.assertTrue(True, 'On exit a coroutine will throw StopIteration')
            self.assertEqual( GtorState.getgeneratorstate(my_coro000), GtorState.GEN_CLOSED)

Upvotes: 0

Rich Frank
Rich Frank

Reputation: 166

To avoid sending a non-None value to a just-started generator, you need to call next or send(None) first. I agree with the others that David Beazley's coroutine decorator (in python 3.x you need to call to __next__() function instead of next()) is a great option. Though that particular decorator is simple, I've also successfully used the copipes library, which is a nice implementation of many of the utilities from Beazley's presentations, including coroutine.

Regarding whether one can check if a generator is started - in Python 3, you can use inspect.getgeneratorstate. This isn't available in Python 2, but the CPython implementation is pure python and doesn't rely on anything new to Python 3, so you can check yourself in the same way:

if generator.gi_running:
    return GEN_RUNNING
if generator.gi_frame is None:
    return GEN_CLOSED
if generator.gi_frame.f_lasti == -1:
    return GEN_CREATED
return GEN_SUSPENDED

Specifically, g2 is started if inspect.getgeneratorstate(g2) != inspect.GEN_CREATED.

Upvotes: 11

Aryeh Leib Taurog
Aryeh Leib Taurog

Reputation: 5598

In particular, you might find a way to use the consumer decorator described on p. I-131 of David Beazley's "Generator Tricks," to which J. Gwyn provided a link:

def consumer(func):
    def start(*args,**kwargs):
        c = func(*args,**kwargs)
        c.next()
        return c
    return start

I use something similar in my code.

Note that if v is None is preferred over if v == None.

Upvotes: 1

Jake Gwyn
Jake Gwyn

Reputation: 51

As your error implies the send function must be called with None on a just-started generator (docs-link).

You could catch the TypeError and roll from there:

    #...
    try:
        g2.send(0)
    except TypeError:
        #Now you know it hasn't started, etc.
        g2.send(None)

Either way it can't be used to 'reset' the generator, it just has to be remade.

Great overview of generator concepts and syntax here, covering chaining of generators and other advanced topics.

Upvotes: 1

Related Questions