Tuesday 3 April 2007

Python Coroutines

The latest release of Python (version 2.5) has a new feature called coroutines. This post looks at what coroutines are and how to use them. Firstly a brief recap on generator functions. The following function is a generator, since it's definition contains the keyword 'yield':

def rota(people):
    _people = list(people)
    current = 0
    while len(_people):
        yield _people[current]
        current = (current + 1) % len(_people)

if __name__ == "__main__":
    people = ["Ant", "Bernard", "Carly", "Deb", "Englebert"]
    r = rota(people)
    for i in range(10):
        print "It's %s's turn." % r.next()

The generator function returns an iterable object, which returns the value given by the yield statement whenever next() is called on it. In the above example, the iterator is never exhausted - it acts like a circular linked list. Note that we create a new list of people - this allows us to pass in an arbitrary sequence or iterator as the initial argument and isolates the generator from other parts of the program changing the list. So what are coroutines? Coroutines are essentially generators which allow you to pass data back into the generator function. For example, lets say we a almost happy with our rota generator, but we would like a way of updating the internal list of people on the rota. This is where coroutines and the 'send()' function come in:

def rota(people):
    _people = list(people)
    current = 0
    while len(_people):
        command = yield _people[current]
        current = (current + 1) % len(_people)
        if command:
            comm, name = command
            if comm == "add":
                _people.append(name)
            elif comm == "remove" and name in _people:
                _people.remove(name)

def printname(name): print "It's %s's turn." % name if __name__ == "__main__": people = ["Ant", "Bernard", "Carly", "Deb", "Englebert"] r = rota(people) for i in range(6): printname(r.next()) printname(r.send(("add", "Fred"))) for i in range(7): printname(r.next()) printname(r.send(("remove","Deb"))) for i in range(6): printname(r.next())

You can see from this example that we can use send() instead of next() to get the next value, but first the argument provided to send is given to the 'command' variable in the coroutine. In this case it expects a pair of things, a command string and a name (note that in a real world example you'd make the coroutine more robust by adding some tests on the return value from the yield). If the command is "add" the name is added to the internal list, if the command is "remove", then the name is removed from the list if it is there.

5 comments:

Allan said...

Excellent example! I have been looking for a simplified explanation of PEP 342. Thanks for your work.

Weng said...

Thank you; I too have benefited from your clear explanation.

AlokOfAarmax said...

Thanks for a most clear and to the point explanation of this concept.

Cd said...

Thank you for the useful example. You have a bug, though. If you remove the last person in the list, just as it would have been their turn, 'current' remains unchanged and you index one past the end of the list.

Cd said...

Thank you for the useful example. You have a bug, though. If you remove the last person in the list, just as it would have been their turn, 'current' remains unchanged and you index one past the end of the list.