[Python-Dev] New PEP 342 suggestion: result() and allow "return with arguments" in generators (was Re: PEP 342 suggestion: start(), __call__() and unwind_call() methods)

Nick Coghlan ncoghlan at gmail.com
Sun Oct 9 15:08:32 CEST 2005

Nick Coghlan wrote:
> Although, if StopIteration.result was a read-only property with the above 
> definition, wouldn't that give us the benefit of "one obvious way" to return a 
> value from a coroutine without imposing any runtime cost on normal use of 
> StopIteration to finish an iterator?

Sometimes I miss the obvious. There's a *much*, *much* better place to store 
the return value of a generator than on the StopIteration exception that it 
raises when it finishes. Just save the return value in the *generator*.

And then provide a method on generators that is the functional equivalent of:

     def result():
         # Finish the generator if it isn't finished already
         for step in self:
         return self._result # Return the result saved when the block finished

It doesn't matter that a for loop swallows the StopIteration exception any 
more, because the return value is retrieved directly from the generator.

I also like that this interface could still be used even if the work of 
getting the result is actually farmed off to a separate thread or process 
behind the scenes.


P.S. Here's what a basic trampoline scheduler without builtin asynchronous 
call support would look like if coroutines could return values directly. The 
bits that it cleans up are marked "NEW":

          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"""

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

              def stop(self):
                  self.running = False

              def schedule(self, coroutine, stack=(), call_result=None, *exc):
                  # Define the new pseudothread
                  def pseudothread():
                          if exc:
                              callee = coroutine.throw(call_result, *exc)
                              callee = coroutine.send(call_result)
                      except StopIteration: # NEW: no need to name exception
                          # Coroutine finished cleanly
                          if stack:
                              # Send the result to the caller
                              caller = stack[0]
                              prev_stack = stack[1]
                              # NEW: get result directly from callee
                                   caller, prev_stack, callee.result()
                          # Coroutine finished with an exception
                          if stack:
                              # send the error back to the caller
                              caller = stack[0]
                              prev_stack = stack[1]
                                   caller, prev_stack, *sys.exc_info()
                              # Nothing left in this pseudothread to
                              # handle it, let it propagate to the
                              # run loop
                          # Coroutine isn't finished yet
                          if callee is None:
                              # Reschedule the current coroutine
                              self.schedule(coroutine, stack)
                          elif isinstance(callee, types.GeneratorType):
                              # Make a call to another coroutine
                              self.schedule(callee, (coroutine,stack))
                          elif iscallable(callee):
                              # Make a blocking call in a separate thread
                                   threaded(callee), (coroutine,stack)
                              # Raise a TypeError in the current coroutine
                              self.schedule(coroutine, stack,
                                   TypeError, "Illegal argument to yield"

                  # Add the new pseudothread to the execution queue

P.P.S. Here's the simple coroutine that threads out a call to support 
asynchronous calls with the above scheduler:

   def threaded(func):
       class run_func(threading.Thread):
           def __init__(self):
               super(run_func, self).__init__()
               self.finished = False
           def run(self):
               print "Making call"
               self.result = func()
               self.finished = True
               print "Made call"
       call = run_func()
       print "Started call"
       while not call.finished:
           yield # Not finished yet so reschedule
       print "Finished call"
       return call.result

I tried this out by replacing 'yield' with 'yield None' and 'return 
call.result' with 'print call.result':

Py> x = threaded(lambda: "Hi there!")
Py> x.next()
Started call
Making call
Made call
Py> x.next()
Finished call
Hi there!
Traceback (most recent call last):
   File "<stdin>", line 1, in ?

Nick Coghlan   |   ncoghlan at gmail.com   |   Brisbane, Australia

More information about the Python-Dev mailing list