[Python-Dev] PEP 340: Non-looping version (aka PEP 310 redux)
Nick Coghlan
ncoghlan at iinet.net.au
Thu May 5 17:03:54 CEST 2005
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 at gmail.com | Brisbane, Australia
---------------------------------------------------------------
http://boredomandlaziness.skystorm.net
More information about the Python-Dev
mailing list