[Python-Dev] Simple coroutines?
Clark C. Evans
cce at clarkevans.com
Tue Aug 24 21:04:22 CEST 2004
A few definitions:
iterator: >
An iterator is an object returned by an __iter__ with
a next() method which produces values. An iterator's
state is kept on the heap (not on the stack), thus
one could consider it 'resumable'. An iterator ends when
it raises an exception, the very special exception,
'StopIteration' is used to signal that the iterator is
done producing values, but is not really an error condition.
generator: >
A syntax for making an iterator, it appears in python
source code as a function or method with a 'yield'
statement. An implementation is free to optimize this
function, but it acts as if it had created an iterator.
iterlink: >
An iterlink is an association of one iterator, the consumer,
to another iterator, the producer. The iterlink contains
information about both iterators, and a marker to indicate
the state of the consumer when it asked for the next value
of the producer.
iterstack: >
A stack of iterlinks, where the producer of one iterlink is
the consumer of another. The top iterator in the iterstack
is an iterator which is behaving like a producer, but not
a consumer. The bottom iterator in the iterstack is one
which is a producer to a non-iterator. That is, the caller
of the bottom iterator is a function or method which is
not an iterator.
Ok. With this in mind, I like your idea of a new keyword, 'suspend'.
This keyword could be used by the top iterator in an iterstack. When
'suspend' happens, the entire iterstack, and its runstate, is setaside.
An a signal is sent to the caller of the bottom iterator; the signal
could be viewed as a 'resumable exception'. For example, it could
be pictured as a 'SuspendIteration' exception. More concretely, using a
very simple case.
class TopIterator:
"""
def TopIterator():
yield "one"
suspend
yield "two"
"""
def _one(self):
self.next = self._suspend
return "one"
def _suspend(self):
self.next = self._two
raise SuspendIteration()
def _two(self):
self.next = self._stop
return "two"
def _stop(self):
raise StopIteration
def __iter__():
self.next = self._one
return self
class BottomIterator():
"""
def BottomIterator():
producer = iter(TopIterator())
saved_one = producer.next()
saved_two = producer.next()
yield (saved_one,saved_two)
"""
def _one(self):
self.saved_one = self.producer.next()
self.next = self._two
return self.next()
def _two(self):
self.saved_two = self.producer.next()
self.next = self._stop
return (self.saved_one, self.saved_two)
def _stop(self):
raise StopIteration
def __iter__(self):
self.producer = iter(TopIterator())
self.next = self._one
return self
def caller():
it = iter(BottomIterator())
while True:
try:
print it.next()
except StopIteration:
break
except SuspendIteration:
sleep(10)
continue
Anyway, the behavior probably equivalent to 'resumable exceptions',
however, I don't think it is _different_ item from an iterator, just a
new set of behaviors triggered by the 'suspend' keyword. As long as the
'iterator construction' is automated, the 'caller()' function can be
delegated to Twisted's reactor, or asynccore or any other cooperative
multitasking base. The superclasses for SuspendIteration could be
'BlockForSocket', for example.
On Tue, Aug 24, 2004 at 05:57:18PM +1200, Greg Ewing wrote:
| This makes me think that maybe we want another kind of object, similar
| to a generator, but designed to be used for side effects rather than
| to produce values.
(a) the generation of 'top-level' iterators using 'suspend'
would be quite easy, see above
(b) the generation logic for "pass-through" generators (aka
BottomIterator in the example above) would have to happen
for the mechanism to be truly useful; SuspendIteration
would have to be a pass-through exception
(c) updating itertools to allow SuspendIteration to 'passthrough'
| For want of a better term, let's call it a "cooperator" (so it ends in
| "ator" like "generator" :-). Actually there will be two objects, a
| cooperator-function (analogous to a generator-function) and a
| cooperator-instance (analogous to a generator-iterator).
|
| Calling a cooperator-function will return a cooperator-instance, which
| will obey the cooperator protocol. This protocol consists of a single
| function run(), which causes the cooperator-instance to perform some
| amount of work and then stop. If there is more work remaining to be
| done, run() returns a true value, otherwise it returns a false value.
Well, run() need not be specified. It would be implemented by the
lower-level 'reactor' code, aka the "caller" in the example
| There will be a new statement:
|
| suspend
|
| for use inside a cooperator-function. The presence of this statement
| is what distinguishes a cooperator-function from an ordinary function
| (just as the presence of 'yield' distinguishes a generator from an
| ordinary function). Execution of 'suspend' will cause the run() method
| to return with a true value. The next time run() is called, execution
| will resume after the 'suspend'.
Right. Only that I'd allow both 'yield' and 'suspend' in the same
function, or it really isn't that useful.
| This is really all that's needed, but one further piece of syntax may
| be desirable to make it easier for one cooperator to invoke another:
|
| do another_coop()
|
| would be equivalent to
|
| co = another_coop()
| while co.run():
| suspend
Exactly; this is where the tough part is, the 'intermediate' cooperators.
Ideally, all generators (since they keep their state on the stack) would
make good 'intermediate' cooperators.
| Something to note is that there's no requirement for a cooperator-
| instance to be implemented by a cooperator-function, just as an
| iterator can be implemented in ways other than a generator. This
| sidesteps some of the difficulties of mixing Python and C calls, since
| a Python cooperator-function could invoke a cooperator implemented in
| C which in turn invokes another Python cooperator- function, etc. The
| only requirement is that all the objects on the way down obey the
| cooperator protocol.
Yes. This is the key, you _only_ attempt to suspend iterators, that is
objects that already keep their state in the heap.
| The drawback is that you always have to know when you're calling
| another cooperator and use 'do'. But that's no worse than the current
| situation with generators, where you always have to know you're using
| an iterator and use 'for... yield'.
Er, well, if the interface was an 'SuspendIteration' then it could be
generalized to all iterators. Afterall, while an exception invalidates
the stack-frame for the current call to next(), it doesn't prevent the
caller from invoking next() once again.
If someone calls next() manually, then SuspendIteration would act as
a regular exception... and be uncaught.
Clark
More information about the Python-Dev
mailing list