Asynchronous application frameworks such as Twisted[1] and Peak[2], are based on a cooperative multitasking via event queues ordeferred execution. While this approach to application developmentdoes not involve threads and thus avoids a whole class of problems[3], it creates a different sort of programming challenge. When anI/O operation would block, a user request must suspend so that otherrequests can proceed. The concept of a coroutine[4] promises tohelp the application developer grapple with this state managementdifficulty.
This PEP proposes a limited approach to coroutines based on anextension to theiterator protocol. Currently, an iterator mayraise a StopIteration exception to indicate that it is done producingvalues. This proposal adds another exception to this protocol,SuspendIteration, which indicates that the given iterator may havemore values to produce, but is unable to do so at this time.
There are two current approaches to bringing co-routines to Python.Christian Tismer’s Stackless[6] involves a ground-up restructuringof Python’s execution model by hacking the ‘C’ stack. While thisapproach works, its operation is hard to describe and keep portable. Arelated approach is to compile Python code to Parrot[7], aregister-based virtual machine, which has coroutines. Unfortunately,neither of these solutions is portable with IronPython (CLR) or Jython(JavaVM).
It is thought that a more limited approach, based on iterators, couldprovide a coroutine facility to application programmers and still beportable across runtimes.
There are four places where this new exception impacts:
While these may seem dramatic changes, it is a very small amount ofwork compared with the utility provided by continuations.
This section will explain, at a high level, how the introduction ofthis new SuspendIteration exception would behave.
The current functionality of iterators is best seen with a simpleexample which produces two values ‘one’ and ‘two’.
classStates:def__iter__(self):self._next=self.state_onereturnselfdefnext(self):returnself._next()defstate_one(self):self._next=self.state_tworeturn"one"defstate_two(self):self._next=self.state_stopreturn"two"defstate_stop(self):raiseStopIterationprintlist(States())
An equivalent iteration could, of course, be created by thefollowing generator:
defStates():yield'one'yield'two'printlist(States())
Suppose that between producing ‘one’ and ‘two’, the generator abovecould block on a socket read. In this case, we would want to raiseSuspendIteration to signal that the iterator is not done producing,but is unable to provide a value at the current moment.
fromrandomimportrandintfromtimeimportsleepclassSuspendIteration(Exception):passclassNonBlockingResource:"""Randomly unable to produce the second value"""def__iter__(self):self._next=self.state_onereturnselfdefnext(self):returnself._next()defstate_one(self):self._next=self.state_suspendreturn"one"defstate_suspend(self):rand=randint(1,10)if2==rand:self._next=self.state_tworeturnself.state_two()raiseSuspendIteration()defstate_two(self):self._next=self.state_stopreturn"two"defstate_stop(self):raiseStopIterationdefsleeplist(iterator,timeout=.1):""" Do other things (e.g. sleep) while resource is unable to provide the next value """it=iter(iterator)retval=[]whileTrue:try:retval.append(it.next())exceptSuspendIteration:sleep(timeout)continueexceptStopIteration:breakreturnretvalprintsleeplist(NonBlockingResource())
In a real-world situation, the NonBlockingResource would be a fileiterator, socket handle, or other I/O based producer. The sleeplistwould instead be an async reactor, such as those found in asyncore orTwisted. The non-blocking resource could, of course, be written as agenerator:
defNonBlockingResource():yield"one"whileTrue:rand=randint(1,10)if2==rand:breakraiseSuspendIteration()yield"two"
It is not necessary to add a keyword, ‘suspend’, since most realcontent generators will not be in application code, they will be inlow-level I/O based operations. Since most programmers need not beexposed to the SuspendIteration() mechanism, a keyword is not needed.
The previous example is rather contrived, a more ‘real-world’ examplewould be a web page generator which yields HTML content, and pullsfrom a database. Note that this is an example of neither the‘producer’ nor the ‘consumer’, but rather of a filter.
defListAlbums(cursor):cursor.execute("SELECT title, artist FROM album")yield'<html><body><table><tr><td>Title</td><td>Artist</td></tr>'for(title,artist)incursor:yield'<tr><td>%s</td><td>%s</td></tr>'%(title,artist)yield'</table></body></html>'
The problem, of course, is that the database may block for some timebefore any rows are returned, and that during execution, rows may bereturned in blocks of 10 or 100 at a time. Ideally, if the databaseblocks for the next set of rows, another user connection could beserviced. Note the complete absence of SuspendIterator in the abovecode. If done correctly, application developers would be able tofocus on functionality rather than concurrency issues.
The iterator created by the above generator should do the magicnecessary to maintain state, yet pass the exception through to alower-level async framework. Here is an example of what thecorresponding iterator would look like if coded up as a class:
classListAlbums:def__init__(self,cursor):self.cursor=cursordef__iter__(self):self.cursor.execute("SELECT title, artist FROM album")self._iter=iter(self._cursor)self._next=self.state_headreturnselfdefnext(self):returnself._next()defstate_head(self):self._next=self.state_cursorreturn"<html><body><table><tr><td>\ Title</td><td>Artist</td></tr>"defstate_tail(self):self._next=self.state_stopreturn"</table></body></html>"defstate_cursor(self):try:(title,artist)=self._iter.next()return'<tr><td>%s</td><td>%s</td></tr>'%(title,artist)exceptStopIteration:self._next=self.state_tailreturnself.next()exceptSuspendIteration:# just pass-throughraisedefstate_stop(self):raiseStopIteration
While the above example is straightforward, things are a bit morecomplicated if the intermediate generator ‘condenses’ values, that is,it pulls in two or more values for each value it produces. Forexample,
defpair(iterLeft,iterRight):rhs=iter(iterRight)lhs=iter(iterLeft)whileTrue:yield(rhs.next(),lhs.next())
In this case, the corresponding iterator behavior has to be a bit moresubtle to handle the case of either the right or left iterator raisingSuspendIteration. It seems to be a matter of decomposing thegenerator to recognize intermediate states where a SuspendIteratorexception from the producing context could happen.
classpair:def__init__(self,iterLeft,iterRight):self.iterLeft=iterLeftself.iterRight=iterRightdef__iter__(self):self.rhs=iter(iterRight)self.lhs=iter(iterLeft)self._temp_rhs=Noneself._temp_lhs=Noneself._next=self.state_rhsreturnselfdefnext(self):returnself._next()defstate_rhs(self):self._temp_rhs=self.rhs.next()self._next=self.state_lhsreturnself.next()defstate_lhs(self):self._temp_lhs=self.lhs.next()self._next=self.state_pairreturnself.next()defstate_pair(self):self._next=self.state_rhsreturn(self._temp_rhs,self._temp_lhs)
This proposal assumes that a corresponding iterator written usingthis class-based method is possible for existing generators. Thechallenge seems to be the identification of distinct states withinthe generator where suspension could occur.
The current generator mechanism has a strange interaction withexceptions where a ‘yield’ statement is not allowed within atry/finally block. The SuspendIterator exception provides anothersimilar issue. The impacts of this issue are not clear. However itmay be that re-writing the generator into a state machine, as theprevious section did, could resolve this issue allowing for thesituation to be no-worse than, and perhaps even removing theyield/finally situation. More investigation is needed in this area.
This proposal only covers ‘suspending’ a chain of iterators, and doesnot cover (of course) suspending general functions, methods, or “C”extension function. While there could be no direct support forcreating generators in “C” code, native “C” iterators which complywith the SuspendIterator semantics are certainly possible.
The author of the PEP is not yet familiar with the Python executionmodel to comment in this area.
This document has been placed in the public domain.
Source:https://github.com/python/peps/blob/main/peps/pep-0334.rst
Last modified:2025-02-01 08:59:27 GMT