[Python-Dev] PEP 342 suggestion: start(), __call__() and unwind_call() methods

Nick Coghlan ncoghlan at gmail.com
Sat Oct 8 03:19:58 CEST 2005


Phillip J. Eby wrote:
> At 09:50 PM 10/7/2005 +1000, Nick Coghlan wrote:
> 
>> Notice how a non-coroutine callable can be yielded, and it will still 
>> work
>> happily with the scheduler, because the desire to continue execution is
>> indicated by the ContinueIteration exception, rather than by the type 
>> of the
>> returned value.
> 
> 
> Whaaaa?  You raise an exception to indicate the *normal* case?  That 
> seems, um...  well, a Very Bad Idea.

The sheer backwardness of my idea occurred to me after I'd got some sleep :)

> Last, but far from least, as far as I can tell you can implement all of 
> these semantics using PEP 342 as it sits.  That is, it's very simple to 
> make decorators or classes that add those semantics.  I don't see 
> anything that requires them to be part of Python.

Yeah, I've now realised that you can do all of this more simply by doing it 
directly in the scheduler using StopIteration to indicate when the coroutine 
is done, and using yield to indicate "I'm not done yet".

So with a bit of thought, I came up with a scheduler that has all the benefits 
I described, and only uses the existing PEP 342 methods.

When writing a coroutine for this scheduler, you can do 6 things via the 
scheduler:

   1. Raise StopIteration to indicate "I'm done" and return None to your caller
   2. Raise StopIteration with a single argument to return a value other than 
None to your caller
   3. Raise a different exception and have that exception propagate up to your 
caller
   5. Yield None to allow other coroutines to be executed
   5. Yield a coroutine to request a call to that coroutine
   6. Yield a callable to request an asynchronous call using that object

Yielding anything else, or trying to raise StopIteration with more than one 
argument results in a TypeError being raised *at the point of the offending 
yield or raise statement*, rather than taking out the scheduler itself.

The more I explore the possibilities of PEP 342, the more impressed I am by 
the work that went into it!

Cheers,
Nick.

P.S. Here's the Trampoline scheduler described above:

         import collections

         class Trampoline:
             """Manage communications between coroutines"""

             running = False

             def __init__(self):
                 self.queue = collections.deque()

             def add(self, coroutine):
                 """Request that a coroutine be executed"""
                 self.schedule(coroutine)

             def run(self):
                 result = None
                 self.running = True
                 try:
                     while self.running and self.queue:
                         func = self.queue.popleft()
                         result = func()
                     return result
                 finally:
                     self.running = False

             def stop(self):
                 self.running = False

             def schedule(self, coroutine, stack=(), call_result=None, *exc):
                 # Define the new pseudothread
                 def pseudothread():
                     try:
                         if exc:
                             callee = coroutine.throw(call_result, *exc)
                         else:
                             callee = coroutine(call_result)
                     except (StopIteration), ex:
                         # Coroutine finished cleanly
                         if stack:
                             # Send the result to the caller
                             caller = stack[0]
                             prev_stack = stack[1]
                             if len(ex.args) > 1:
                                 # Raise a TypeError in the current coroutine
                                 self.schedule(coroutine, stack,
                                      TypeError,
                                      "Too many arguments to StopIteration"
                                 )
                             elif ex.args:
                                 self.schedule(caller, prev_stack, ex.args[0])
                             else:
                                 self.schedule(caller, prev_stack)
                     except:
                         # Coroutine finished with an exception
                         if stack:
                             # send the error back to the caller
                             caller = stack[0]
                             prev_stack = stack[1]
                             self.schedule(
                                  caller, prev_stack, *sys.exc_info()
                             )
                         else:
                             # Nothing left in this pseudothread to
                             # handle it, let it propagate to the
                             # run loop
                             raise
                     else:
                         # Coroutine isn't finished yet
                         if callee is None:
                             # Reschedule the current coroutine
                             self.schedule(coroutine, stack)
                         elif isinstance(callee, types.GeneratorType):
                             # Requested a call to another coroutine
                             self.schedule(callee, (coroutine,stack))
                         elif callable(callee):
                             # Requested an asynchronous call
                             self._make_async_call(callee, coroutine, stack)
                         else:
                             # Raise a TypeError in the current coroutine
                             self.schedule(coroutine, stack,
                                  TypeError, "Illegal argument to yield"
                             )

                 # Add the new pseudothread to the execution queue
                 self.queue.append(pseudothread)

             def _make_async_call(self, blocking_call, caller, stack):
                 # Assume @threaded decorator takes care of
                 #   - returning a function with a call method which
                 #     kick starts the function execution and returns
                 #     a Future object to give access to the result.
                 #   - farming call out to a physical thread pool
                 #   - keeping the Thread object executing the async
                 #     call alive after this function exits
                 @threaded
                 def async_call():
                     try:
                         result = blocking_call()
                     except:
                         # send the error back to the caller
                         self.schedule(
                             caller, stack, *sys.exc_info()
                         )
                     else:
                         # Send the result back to the caller
                         self.schedule(caller, stack, result)

                 # Start the asynchronous call
                 async_call()


-- 
Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia
---------------------------------------------------------------
             http://boredomandlaziness.blogspot.com


More information about the Python-Dev mailing list