[Python-Dev] Pre-PEP: Task-local variables

Nick Coghlan ncoghlan at gmail.com
Thu Oct 20 14:40:07 CEST 2005


Phillip J. Eby wrote:
> This is still rather rough, but I figured it's easier to let everybody fill 
> in the remaining gaps by arguments than it is for me to pick a position I 
> like and try to convince everybody else that it's right.  :)  Your feedback 
> is requested and welcome.

I think you're actually highlighting a bigger issue with the behaviour of 
"yield" inside a "with" block, and working around it rather than fixing the 
fundamental problem.

The issue with "yield" causing changes to leak to outer scopes isn't limited 
to coroutine style usage - it can happen with generator-iterators, too.

What's missing is a general way of saying "suspend this context temporarily, 
and resume it when done".

An example use-case not involving 'yield' at all is the "asynchronise" 
functionality. A generator-iterator that works in a high precision 
decimal.Context(), but wants to return values from inside a loop using normal 
precision is another example not involving coroutines.

The basic idea would be to provide syntax that allows a with statement to be 
"suspended", along the lines of:

   with EXPR as VAR:
       for VAR2 in EXPR2:
           without:
               BLOCK

To mean:

   abc = (EXPR).__with__()
   exc = (None, None, None)
   VAR = abc.__enter__()
   try:
       for VAR2 in EXPR2:
           try:
               abc.__suspend__()
               try:
                   BLOCK
               finally:
                   abc.__resume__()
           except:
               exc = sys.exc_info()
               raise
   finally:
       abc.__exit__(*exc)


To keep things simple, just as 'break' and 'continue' work only on the 
innermost loop, 'without' would only apply to the innermost 'with' statement.

Locks, for example, could support this via:

   class Lock(object):
     def __with__(self):
         return self

     def __enter__(self):
         self.acquire()
         return self

     def __resume__(self):
         self.acquire()

     def __suspend__(self):
         self.release()

     def __exit__(self):
         self.release()


(Note that there's a potential problem if the call to acquire() in __resume__ 
fails, but that's no different than if this same dance is done manually).

Cheers,
Nick.

P.S. Here's a different generator wrapper that could be used to create a 
generator-based "suspendable context" that can be invoked multiple times 
through use of the "without" keyword. If applied to the PEP 343 
decimal.Context() __with__ method example, it would automatically restore the 
original context for the duration of the "without" block:

   class SuspendableGeneratorContext(object):

      def __init__(self, func, args, kwds):
          self.gen = None
          self.func = func
          self.args = args
          self.kwds = kwds

      def __with__(self):
          return self

      def __enter__(self):
          if self.gen is not None:
              raise RuntimeError("context already in use")
          gen = self.func(*args, **kwds)
          try:
              result = gen.next()
          except StopIteration:
              raise RuntimeError("generator didn't yield")
          self.gen = gen
          return result


      def __resume__(self):
          if self.gen is None:
              raise RuntimeError("context not suspended")
          gen = self.func(*args, **kwds)
          try:
              gen.next()
          except StopIteration:
              raise RuntimeError("generator didn't yield")
          self.gen = gen

      def __suspend__(self):
          try:
              self.gen.next()
          except StopIteration:
              return
          else:
              raise RuntimeError("generator didn't stop")

      def __exit__(self, type, value, traceback):
          gen = self.gen
          self.gen = None
          if type is None:
              try:
                  gen.next()
              except StopIteration:
                  return
              else:
                  raise RuntimeError("generator didn't stop")
          else:
              try:
                  gen.throw(type, value, traceback)
              except (type, StopIteration):
                  return
              else:
                  raise RuntimeError("generator caught exception")

   def suspendable_context(func):
      def helper(*args, **kwds):
          return SuspendableGeneratorContext(func, args, kwds)
      return helper

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


More information about the Python-Dev mailing list