[prev in list] [next in list] [prev in thread] [next in thread] 

List:       python-dev
Subject:    [Python-Dev] PEP 340: Non-looping version (aka PEP 310 redux)
From:       Nick Coghlan <ncoghlan () iinet ! net ! au>
Date:       2005-05-05 15:03:54
Message-ID: 427A35DA.8050505 () iinet ! net ! au
[Download RAW message or body]

The discussion on the meaning of break when nesting a PEP 340 block statement 
inside a for loop has given me some real reasons to prefer PEP 310's single pass 
semantics for user defined statements (more on that at the end). The suggestion 
below is my latest attempt at combining the ideas of the two PEP's.

For the keyword, I've used the abbreviation 'stmt' (for statement). I find it 
reads pretty well, and the fact that it *isn't* a real word makes it easier for 
me to track to the next item on the line to find out the actual statement name 
(I think this might be similar to the effect of 'def' not being a complete word 
making it easier for me to pick out the function name). I consequently use 'user 
statement' or 'user defined statement' to describe what PEP 340 calls anonymous 
block statements.

I'm still fine with the concept of not using a keyword at all, though.

Cheers,
Nick.

== User Defined Statement Usage Syntax ==

   stmt EXPR1 [as VAR1]:
       BLOCK1


== User Defined Statement Semantics ==

   the_stmt = EXPR1
   terminated = False
   try:
       stmt_enter = the_stmt.__enter__
       stmt_exit = the_stmt.__exit__
   except AttributeError:
       raise TypeError("User statement required")
   try:
       VAR1 = stmt_enter() # Omit 'VAR1 =' if no 'as' clause
   except TerminateBlock:
       pass
       # Block is not entered at all in this case
       # If an else clause were to be permitted, the
       # associated block would be executed here
   else:
       try:
           try:
               BLOCK1
           except:
               exc = sys.exc_info()
               terminated = True
               try:
                   stmt_exit(*exc)
               except TerminateBlock:
                   pass
       finally:
           if not terminated:
               try:
                   stmt_exit(TerminateBlock)
               except TerminateBlock:
                   pass

Key points:
* The supplied expression must have both __enter__ and __exit__ methods.
* The result of the __enter__ method is assigned to VAR1 if VAR1 is given.
* BLOCK1 is not executed if __enter__ raises an exception
* A new exception, TerminateBlock, is used to signal statement completion
* The __exit__ method is called with the exception tuple if an exception occurs
* Otherwise it is called with TerminateBlock as the argument
* The __exit__ method can suppress an exception by converting it to 
TerminateBlock or by returning without reraising the exception
* return, break, continue and raise StopIteration are all OK inside BLOCK1. They 
affect the surrounding scope, and are in no way tampered with by the user 
defined statement machinery (some user defined statements may choose to suppress 
the raising of StopIteration, but the basic machinery doesn't do that)
* Decouples user defined statements from yield expressions, the enhanced 
continue statement and generator finalisation.

== New Builtin: statement ==

   def statement(factory):
       try:
          factory.__enter__
          factory.__exit__
          # Supplied factory is already a user statement factory
          return factory
       except AttributeError:
          # Assume supplied factory is an iterable factory
          # Use it to create a user statement factory
          class stmt_factory(object):
              def __init__(*args, **kwds)
                  self = args[0]
                  self.itr = iter(factory(*args[1:], **kwds))
              def __enter__(self):
                  try:
                      return self.itr.next()
                  except StopIteration:
                      raise TerminateBlock
              def __exit__(self, *exc_info):
                  try:
                      stmt_exit = self.itr.__exit__
                  except AttributeError:
                      try:
                          self.itr.next()
                      except StopIteration:
                          pass
                      raise *exc_info # i.e. re-raise the supplied exception
                  else:
                      try:
                          stmt_exit(*exc_info)
                      except StopIteration:
                          raise TerminateBlock

Key points:
* The supplied factory is returned unchanged if it supports the statement API 
(such as a class with both __enter__ and __exit__ methods)
* An iterable factory (such as a generator, or class with an __iter__ method) is 
converted to a block statement factory
* Either way, the result is a callable whose results can be used as EXPR1 in a 
user defined statement.
* For statements constructed from iterators, the iterator's next() method is 
called once when entering the statement, and the result is assigned to VAR1
* If the iterator has an __exit__ method, it is invoked when the statement is 
exited. The __exit__ method is passed the exception information (which may 
indicate that no exception occurred).
* If the iterator does not have an __exit__ method, it's next() method is 
invoked a second time instead
* When an iterator is used to drive a user defined statement, StopIteration is 
translated to TerminateBlock
* Main intended use is as a generator decorator
* Decouples user defined statements from yield expressions, the enhanced 
continue statement and generator finalisation.

== Justification for non-looping semantics ==

For most use cases, the effect PEP 340 block statements have on break and 
continue statements is both surprising and undesirable. This is highlighted by 
the major semantic difference between the following two cases:

   stmt locking(lock):
       for item in items:
           if handle(item):
               break

   for item in items:
       stmt locking(lock):
           if handle(item):
               break

Instead of simply acquiring and releasing the lock on each iteration, as one 
would legitimately expect, the latter piece of code actually processes all of 
the items, instead of breaking out of the loop once one of the items is handled. 
With non-looping user defined statements, the above code works in the obvious 
fashion (the break statement ends the for loop, not the lock acquisition).

With non-looping semantics, the implementation of the examples in PEP 340 is 
essentially identical - just add an invocation of @statement to the start of the 
generators. It also becomes significantly easier to write user defined 
statements manually as there is no need to track state:

   class locking:
       def __init__(self, lock):
           self.lock = lock
       def __enter__(self):
           self.lock.acquire()
       def __exit__(self, exc_type, value=None, traceback=None):
           self.lock.release()
           if type is not None:
               raise exc_type, value, traceback

The one identified use case for a user-defined loop was PJE's auto_retry. We 
already have user-defined loops in the form of custom iterators, and there is 
nothing stopping an iterator from returning user defined statements like this:

   for attempt in auto_retry(3, IOError):
       stmt attempt:
           # Do something!
           # Including break to give up early
           # Or continue to try again without raising IOError

The implementation of auto-retry is messier than it is with all user defined 
statement being loops, but I think the benefits of non-looping semantics justify 
that sacrifice. Besides, it really isn't all that bad:

   class auto_retry(3, IOError):
       def __init__(self, times, exc=Exception):
           self.times = xrange(times-1)
           self.exc = exc
           self.succeeded = False

       def __iter__(self):
           attempt = self.attempt
           for i in self.times:
               yield attempt()
               if self.succeeded:
                   break
           else:
               yield self.last_attempt()

       @statement
       def attempt(self):
           try:
               yield None
               self.succeeded = True
           except self.exc:
               pass

       @statement
       def last_attempt(self):
           yield None

(Third time lucky! One day I'll remember that Python has these things called 
classes designed to elegantly share state between a collection of related 
functions and generators. . .)

The above code for auto_retry assumes that generators supply an __exit__ method 
as described in PEP 340 - without that, auto_retry.attempt would need to be 
written as a class since it needs to know if an exception was thrown or not:

   class auto_retry(3, IOError):
       def __init__(self, times, exc=Exception):
           self.times = xrange(times-1)
           self.exc = exc
           self.succeeded = False

       def __iter__(self):
           attempt = self.attempt
           for i in self.times:
               yield attempt(self)
               if self.succeeded:
                   break
           else:
               yield self.last_attempt()

       class attempt(object):
           def __init__(self, outer):
               self.outer = outer
           def __enter__(self):
               pass
           def __exit__(self, exc_type, value=None, traceback=None):
               if exc_type is None:
                   self.outer.succeeded = true
               elif exc_type not in self.outer.exc
                   raise exc_type, value, traceback

       @statement
       def last_attempt(self):
           yield None

-- 
Nick Coghlan   |   ncoghlan@gmail.com   |   Brisbane, Australia
---------------------------------------------------------------
             http://boredomandlaziness.skystorm.net
_______________________________________________
Python-Dev mailing list
Python-Dev@python.org
http://mail.python.org/mailman/listinfo/python-dev
Unsubscribe: http://mail.python.org/mailman/options/python-dev/python-dev%40progressive-comp.com
[prev in list] [next in list] [prev in thread] [next in thread] 

Configure | About | News | Add a list | Sponsored by KoreLogic