Update of /cvsroot/python/python/nondist/peps
In directory sc8-pr-cvs1.sourceforge.net:/tmp/cvs-serv22078
Modified Files:
pep-0343.txt
Log Message:
Specify generator enhancements. Change keyword to 'with'.
Index: pep-0343.txt
===================================================================
RCS file: /cvsroot/python/python/nondist/peps/pep-0343.txt,v
retrieving revision 1.16
retrieving revision 1.17
diff -u -d -r1.16 -r1.17
--- pep-0343.txt 31 May 2005 20:27:15 -0000 1.16
+++ pep-0343.txt 1 Jun 2005 15:13:37 -0000 1.17
@@ -1,5 +1,5 @@
PEP: 343
-Title: Anonymous Block Redux
+Title: Anonymous Block Redux and Generator Enhancements
Version: $Revision$
Last-Modified: $Date$
Author: Guido van Rossum
@@ -11,17 +11,13 @@
Introduction
- After a lot of discussion about PEP 340 and alternatives, I've
- decided to withdraw PEP 340 and propose a slight variant on
- PEP 310.
-
-Evolutionary Note
-
- After ample discussion on python-dev, I'll add back a mechanism
+ After a lot of discussion about PEP 340 and alternatives, I
+ decided to withdraw PEP 340 and proposed a slight variant on PEP
+ 310. After more discussion, I have added back a mechanism
for raising an exception in a suspended generator using a throw()
method, and a close() method which throws a new GeneratorExit
- exception. Until I get a chance to update the PEP, see reference
- [2]. I'm also leaning towards 'with' as the keyword.
+ exception; these additions were first proposed in [2] and
+ universally approved of. I'm also changing the keyword to 'with'.
Motivation and Summary
@@ -53,7 +49,7 @@
with VAR = EXPR:
BLOCK
- which roughly translates into
+ which roughly translates into this:
VAR = EXPR
VAR.__enter__()
@@ -74,7 +70,7 @@
goto (a break, continue or return), BLOCK2 is *not* reached. The
magic added by the with-statement at the end doesn't affect this.
- (You may ask, what if a bug in the __exit__ method causes an
+ (You may ask, what if a bug in the __exit__() method causes an
exception? Then all is lost -- but this is no worse than with
other exceptions; the nature of exceptions is that they can happen
*anywhere*, and you just have to live with that. Even if you
@@ -89,16 +85,18 @@
Inspired by a counter-proposal to PEP 340 by Phillip Eby I tried
to create a decorator that would turn a suitable generator into an
- object with the necessary __entry__ and __exit__ methods. Here I
- ran into a snag: while it wasn't too hard for the locking example,
- it was impossible to do this for the opening example. The idea
- was to define the template like this:
+ object with the necessary __enter__() and __exit__() methods.
+ Here I ran into a snag: while it wasn't too hard for the locking
+ example, it was impossible to do this for the opening example.
+ The idea was to define the template like this:
@with_template
def opening(filename):
f = open(filename)
- yield f
- f.close()
+ try:
+ yield f
+ finally:
+ f.close()
and used it like this:
@@ -106,21 +104,21 @@
...read data from f...
The problem is that in PEP 310, the result of calling EXPR is
- assigned directly to VAR, and then VAR's __exit__ method is called
- upon exit from BLOCK1. But here, VAR clearly needs to receive the
- opened file, and that would mean that __exit__ would have to be a
- method on the file.
+ assigned directly to VAR, and then VAR's __exit__() method is
+ called upon exit from BLOCK1. But here, VAR clearly needs to
+ receive the opened file, and that would mean that __exit__() would
+ have to be a method on the file.
While this can be solved using a proxy class, this is awkward and
made me realize that a slightly different translation would make
writing the desired decorator a piece of cake: let VAR receive the
- result from calling the __enter__ method, and save the value of
- EXPR to call its __exit__ method later. Then the decorator can
- return an instance of a wrapper class whose __enter__ method calls
- the generator's next() method and returns whatever next() returns;
- the wrapper instance's __exit__ method calls next() again but
- expects it to raise StopIteration. (Details below in the section
- Optional Generator Decorator.)
+ result from calling the __enter__() method, and save the value of
+ EXPR to call its __exit__() method later. Then the decorator can
+ return an instance of a wrapper class whose __enter__() method
+ calls the generator's next() method and returns whatever next()
+ returns; the wrapper instance's __exit__() method calls next()
+ again but expects it to raise StopIteration. (Details below in
+ the section Optional Generator Decorator.)
So now the final hurdle was that the PEP 310 syntax:
@@ -128,41 +126,61 @@
BLOCK1
would be deceptive, since VAR does *not* receive the value of
- EXPR. Given PEP 340, it was an easy step to:
+ EXPR. Borrowing from PEP 340, it was an easy step to:
with EXPR as VAR:
BLOCK1
- or, using an alternate keyword that has been proposed a number of
- times:
+ Additional discussion showed that people really liked being able
+ to "see" the exception in the generator, even if it was only to
+ log it; the generator is not allowed to yield another value, since
+ the with-statement should not be usable as a loop (raising a
+ different exception is marginally acceptable). To enable this, a
+ new throw() method for generators is proposed, which takes three
+ arguments representing an exception in the usual fashion (type,
+ value, traceback) and raises it at the point where the generator
+ is suspended.
- do EXPR as VAR:
- BLOCK1
+ Once we have this, it is a small step to proposing another
+ generator method, close(), which calls throw() with a special
+ exception, GeneratorExit. This tells the generator to exit, and
+ from there it's another small step to proposing that close() be
+ called automatically when the generator is garbage-collected.
+
+ Then, finally, we can allow a yield-statement inside a try-finally
+ statement, since we can now guarantee that the finally-clause will
+ (eventually) be executed. The usual cautions about finalization
+ apply -- the process may be terminated abruptly without finalizing
+ any objects, and objects may be kept alive forever by cycles or
+ memory leaks in the application (as opposed to cycles or leaks in
+ the Python implementation, which are taken care of by GC).
+
+ Note that we're not guaranteeing that the finally-clause is
+ executed immediately after the generator object becomes unused,
+ even though this is how it will work in CPython. This is similar
+ to auto-closing files: while a reference-counting implementation
+ like CPython deallocates an object as soon as the last reference
+ to it goes away, implementations that use other GC algorithms do
+ not make the same guarantee. This applies to Jython, IronPython,
+ and probably to Python running on Parrot.
Use Cases
See the Examples section near the end.
-Specification
+Specification: The 'with' Statement
A new statement is proposed with the syntax:
- do EXPR as VAR:
+ with EXPR as VAR:
BLOCK
- Here, 'do' and 'as' are new keywords; EXPR is an arbitrary
+ Here, 'with' and 'as' are new keywords; EXPR is an arbitrary
expression (but not an expression-list) and VAR is an arbitrary
assignment target (which may be a comma-separated list).
The "as VAR" part is optional.
- The choice of the 'do' keyword is provisional; an alternative
- under consideration is 'with'.
-
- A yield-statement is illegal inside BLOCK. This is because the
- do-statement is translated into a try/finally statement, and yield
- is illegal in a try/finally statement.
-
The translation of the above statement is:
abc = EXPR
@@ -209,94 +227,184 @@
non-local goto should be considered unexceptional for the purposes
of a database transaction roll-back decision.
-Optional Generator Decorator
+Specification: Generator Enhancements
+
+ Let a generator object be the iterator produced by calling a
+ generator function. Below, 'g' always refers to a generator
+ object.
+
+ New syntax: yield allowed inside try-finally
+
+ The syntax for generator functions is extended to allow a
+ yield-statement inside a try-finally statement.
+
+ New generator method: throw(type, value, traceback)
+
+ g.throw(type, value, traceback) causes the specified exception to
+ be thrown at the point where the generator g is currently
+ suspended (i.e. at a yield-statement, or at the start of its
+ function body if next() has not been called yet). If the
+ generator catches the exception and yields another value, that is
+ the return value of g.throw(). If it doesn't catch the exception,
+ the throw() appears to raise the same exception passed it (it
+ "falls through"). If the generator raises another exception (this
+ includes the StopIteration produced when it returns) that
+ exception is raised by the throw() call. In summary, throw()
+ behaves like next() except it raises an exception at the
+ suspension point. If the generator is already in the closed
+ state, throw() just raises the exception it was passed without
+ executing any of the generator's code.
+
+ The effect of raising the exception is exactly as if the
+ statement:
+
+ raise type, value, traceback
+
+ was executed at the suspension point. The type argument should
+ not be None.
+
+ New standard exception: GeneratorExit
+
+ A new standard exception is defined, GeneratorExit, inheriting
+ from Exception. A generator should handle this by re-raising it
+ or by raising StopIteration.
+
+ New generator method: close()
+
+ g.close() is defined by the following pseudo-code:
+
+ def close(self):
+ try:
+ self.throw(GeneratorExit, GeneratorExit(), None)
+ except (GeneratorExit, StopIteration):
+ pass
+ else:
+ raise TypeError("generator ignored GeneratorExit")
+ # Other exceptions are not caught
+
+ New generator method: __del__()
+
+ g.__del__() is an alias for g.close(). This will be called when
+ the generator object is garbage-collected (in CPython, this is
+ when its reference count goes to zero). If close() raises an
+ exception, a traceback for the exception is printed to sys.stderr
+ and further ignored; it is not propagated back to the place that
+ triggered the garbage collection. This is consistent with the
+ handling of exceptions in __del__() methods on class instances.
+
+ If the generator object participates in a cycle, g.__del__() may
+ not be called. This is the behavior of CPython's current garbage
+ collector. The reason for the restriction is that the GC code
+ needs to "break" a cycle at an arbitrary point in order to collect
+ it, and from then on no Python code should be allowed to see the
+ objects that formed the cycle, as they may be in an invalid state.
+ Objects "hanging off" a cycle are not subject to this restriction.
+ Note that it is unlikely to see a generator object participate in
+ a cycle in practice. However, storing a generator object in a
+ global variable creates a cycle via the generator frame's
+ f_globals pointer. Another way to create a cycle would be to
+ store a reference to the generator object in a data structure that
+ is passed to the generator as an argument. Neither of these cases
+ are very likely given the typical pattern of generator use.
+
+Generator Decorator
It is possible to write a decorator that makes it possible to use
- a generator that yields exactly once to control a do-statement.
+ a generator that yields exactly once to control a with-statement.
Here's a sketch of such a decorator:
class Wrapper(object):
+
def __init__(self, gen):
self.gen = gen
- self.state = "initial"
+
def __enter__(self):
- assert self.state == "initial"
- self.state = "entered"
try:
return self.gen.next()
except StopIteration:
- self.state = "error"
- raise RuntimeError("template generator didn't yield")
- def __exit__(self, *args):
- assert self.state == "entered"
- self.state = "exited"
- try:
- self.gen.next()
- except StopIteration:
- return
+ raise RuntimeError("generator didn't yield")
+
+ def __exit__(self, type, value, traceback):
+ if type is None:
+ try:
+ self.gen.next()
+ except StopIteration:
+ return
+ else:
+ raise RuntimeError("generator didn't stop")
else:
- self.state = "error"
- raise RuntimeError("template generator didn't stop")
+ try:
+ self.gen.throw(type, value, traceback)
+ except (type, StopIteration):
+ return
+ else:
+ raise RuntimeError("generator caught exception")
- def do_template(func):
+ def with_template(func):
def helper(*args, **kwds):
return Wrapper(func(*args, **kwds))
return helper
This decorator could be used as follows:
- @do_template
+ @with_template
def opening(filename):
f = open(filename) # IOError here is untouched by Wrapper
yield f
f.close() # Ditto for errors here (however unlikely)
- A robust implementation of such a decorator should be made part of
- the standard library.
+ A robust implementation of this decorator should be made part of
+ the standard library, but not necessarily as a built-in function.
+ (I'm not sure which exception it should raise for errors;
+ RuntimeError is used above as an example only.)
-Other Optional Extensions
+Optional Extensions
It would be possible to endow certain objects, like files,
- sockets, and locks, with __enter__ and __exit__ methods so that
- instead of writing:
+ sockets, and locks, with __enter__() and __exit__() methods so
+ that instead of writing:
- do locking(myLock):
+ with locking(myLock):
BLOCK
one could write simply:
- do myLock:
+ with myLock:
BLOCK
I think we should be careful with this; it could lead to mistakes
like:
f = open(filename)
- do f:
+ with f:
BLOCK1
- do f:
+ with f:
BLOCK2
- which does not do what one might think (f is closed when BLOCK2 is
- entered).
+ which does not do what one might think (f is closed before BLOCK2
+ is entered).
+
+ OTOH such mistakes are easily diagnosed.
Examples
- Several of these examples contain "yield None". If PEP 342 is
- accepted, these can be changed to just "yield".
+ (Note: several of these examples contain "yield None". If PEP 342
+ is accepted, these can be changed to just "yield".)
1. A template for ensuring that a lock, acquired at the start of a
block, is released when the block is left:
- @do_template
+ @with_template
def locking(lock):
lock.acquire()
- yield None
- lock.release()
+ try:
+ yield None
+ finally:
+ lock.release()
Used as follows:
- do locking(myLock):
+ with locking(myLock):
# Code here executes with myLock held. The lock is
# guaranteed to be released when the block is left (even
# if via return or by an uncaught exception).
@@ -304,29 +412,32 @@
2. A template for opening a file that ensures the file is closed
when the block is left:
- @do_template
+ @with_template
def opening(filename, mode="r"):
f = open(filename, mode)
- yield f
- f.close()
+ try:
+ yield f
+ finally:
+ f.close()
Used as follows:
- do opening("/etc/passwd") as f:
+ with opening("/etc/passwd") as f:
for line in f:
print line.rstrip()
3. A template for committing or rolling back a database
transaction; this is written as a class rather than as a
- decorator since it requires access to the exception information:
+ decorator since it requires access to the exception
+ information:
class transactional:
def __init__(self, db):
self.db = db
def __enter__(self):
self.db.begin()
- def __exit__(self, *args):
- if args and args[0] is not None:
+ def __exit__(self, type, value, tb):
+ if type is not None:
self.db.rollback()
else:
self.db.commit()
@@ -338,47 +449,51 @@
self.lock = lock
def __enter__(self):
self.lock.acquire()
- def __exit__(self, *args):
+ def __exit__(self, type, value, tb):
self.lock.release()
(This example is easily modified to implement the other
- examples; it shows how much simpler generators are for the same
- purpose.)
+ examples; it shows the relative advantage of using a generator
+ template.)
5. Redirect stdout temporarily:
- @do_template
+ @with_template
def redirecting_stdout(new_stdout):
save_stdout = sys.stdout
sys.stdout = new_stdout
- yield None
- sys.stdout = save_stdout
+ try:
+ yield None
+ finally:
+ sys.stdout = save_stdout
Used as follows:
- do opening(filename, "w") as f:
- do redirecting_stdout(f):
+ with opening(filename, "w") as f:
+ with redirecting_stdout(f):
print "Hello world"
This isn't thread-safe, of course, but neither is doing this
- same dance manually. In a single-threaded program (e.g., a
- script) it is a totally fine way of doing things.
+ same dance manually. In single-threaded programs (for example,
+ in scripts) it is a popular way of doing things.
6. A variant on opening() that also returns an error condition:
- @do_template
+ @with_template
def opening_w_error(filename, mode="r"):
try:
f = open(filename, mode)
except IOError, err:
yield None, err
else:
- yield f, None
- f.close()
+ try:
+ yield f, None
+ finally:
+ f.close()
Used as follows:
- do opening_w_error("/etc/passwd", "a") as f, err:
+ with opening_w_error("/etc/passwd", "a") as f, err:
if err:
print "IOError:", err
else:
@@ -389,7 +504,7 @@
import signal
- do signal.blocking():
+ with signal.blocking():
# code executed without worrying about signals
An optional argument might be a list of signals to be blocked;
@@ -401,19 +516,21 @@
import decimal
- @do_template
- def with_extra_precision(places=2):
+ @with_template
+ def extra_precision(places=2):
c = decimal.getcontext()
saved_prec = c.prec
c.prec += places
- yield None
- c.prec = saved_prec
+ try:
+ yield None
+ finally:
+ c.prec = saved_prec
Sample usage (adapted from the Python Library Reference):
def sin(x):
"Return the sine of x as measured in radians."
- do with_extra_precision():
+ with extra_precision():
i, lasts, s, fact, num, sign = 1, 0, x, 1, x, 1
while s != lasts:
lasts = s
@@ -423,31 +540,33 @@
sign *= -1
s += num / fact * sign
# The "+s" rounds back to the original precision,
- # so this must be outside the do-statement:
+ # so this must be outside the with-statement:
return +s
9. Here's a more general Decimal-context-switching template:
- @do_template
- def with_decimal_context(newctx=None):
+ @with_template
+ def decimal_context(newctx=None):
oldctx = decimal.getcontext()
if newctx is None:
newctx = oldctx.copy()
decimal.setcontext(newctx)
- yield newctx
- decimal.setcontext(oldctx)
+ try:
+ yield newctx
+ finally:
+ decimal.setcontext(oldctx)
- Sample usage (adapted from the previous one):
+ Sample usage:
def sin(x):
- do with_decimal_context() as ctx:
+ with decimal_context() as ctx:
ctx.prec += 2
- # Rest of algorithm the same
+ # Rest of algorithm the same as above
return +s
- (Nick Coghlan has proposed to add __enter__ and __exit__
+ (Nick Coghlan has proposed to add __enter__() and __exit__()
methods to the decimal.Context class so that this example can
- be simplified to "do decimal.getcontext() as ctx: ...".)
+ be simplified to "with decimal.getcontext() as ctx: ...".)
References