Establish the "alternative" version. The exception API is called

__exit__(), and its signature is the same as that of the
raise-statement.  Still some loose ends.
This commit is contained in:
Guido van Rossum 2005-05-02 03:30:07 +00:00
parent b63ff845a3
commit a7de7b62af
1 changed files with 114 additions and 194 deletions

View File

@ -26,30 +26,6 @@ Introduction
this on python-dev recently [1], and I figured it would be time to this on python-dev recently [1], and I figured it would be time to
write up a precise spec in PEP form. write up a precise spec in PEP form.
Proposal Evolution
The discussion on python-dev has changed my mind slightly on how
exceptions should be handled, but I don't have the time to do a
full update of the PEP right now. Basically, I'm now in favor of
a variation on the exception handling proposed in the section
"Alternative __next__() and Generator Exception Handling" below.
The added twist is that instead of adding a flag argument to
next() and __next__() to indicate whether the previous argument is
a value or an exception, we use a separate API (an __exit__()
method taking an exception and perhaps a traceback) for the
exception. If an iterator doesn't implement __exit__(), the
exception is just re-raised. It is expected that, apart from
generators, very few iterators will implement __exit__(); one use
case would be a fast implementation of synchronized() written in
C.
The built-in next() function only interfaces to the next() and
__next__() methods; there is no user-friendly API to call
__exit__(). (Or perhaps calling next(itr, exc, traceback) would
call itr.__exit__(exc, traceback) if itr has an __exit__ method
and otherwise raise exc.__class__, exc, traceback?)
Motivation and Summary Motivation and Summary
(Thanks to Shane Hathaway -- Hi Shane!) (Thanks to Shane Hathaway -- Hi Shane!)
@ -96,40 +72,25 @@ Use Cases
TBD. For now, see the Examples section near the end. TBD. For now, see the Examples section near the end.
Specification: the Iteration Exception Hierarchy
Two new built-in exceptions are defined, and StopIteration is
moved in the exception hierarchy:
class Iteration(Exception):
pass
class StopIteration(Iteration):
pass
class ContinueIteration(Iteration):
def __init__(self, value=None):
self.value = None
Specification: the __next__() Method Specification: the __next__() Method
A new method for iterators is proposed, called __next__(). It A new method for iterators is proposed, called __next__(). It
takes one optional argument, which defaults to None. If not None, takes one optional argument, which defaults to None. Calling the
the argument must be an Iteration instance. Calling the
__next__() method without argument or with None is equivalent to __next__() method without argument or with None is equivalent to
using the old iterator API, next(). For backwards compatibility, using the old iterator API, next(). For backwards compatibility,
it is recommended that iterators also implement a next() method as it is recommended that iterators also implement a next() method as
an alias for calling the __next__() method without an argument. an alias for calling the __next__() method without an argument.
Calling the __next__() method with a StopIteration instance The argument to the __next__() method may be used by the iterator
signals the iterator that the caller wants to abort the iteration as a hint on what to do next.
sequence; the iterator should respond by doing any necessary
cleanup and raising StopIteration. Calling it with a Specification: the __exit__() Method
ContinueIteration instance signals the iterator that the caller
wants to continue the iteration; the ContinueIteration exception An optional new method for iterators is proposed, called
has a 'value' attribute which may be used by the iterator as a __exit__(). It takes up to three arguments which correspond to
hint on what to do next. Calling it with a (base class) Iteration the three "arguments" to the raise-statement: type, value, and
instance is the same as calling it with None. traceback. If all three arguments are None, sys.exc_info() may be
consulted to provide suitable default values.
Specification: the next() Built-in Function Specification: the next() Built-in Function
@ -143,34 +104,43 @@ Specification: the next() Built-in Function
return itr.next() return itr.next()
raise TypeError("next() with arg for old-style iterator") raise TypeError("next() with arg for old-style iterator")
Specification: the 'for' Loop This function is proposed because there is often a need to call
the next() method outside a for-loop; the new API, and the
backwards compatibility code, is too ugly to have to repeat in
user code.
Note that I'm not proposing a built-in function to call the
__exit__() method of an iterator. I don't expect that this will
be called much outside the block-statement.
Specification: a Change to the 'for' Loop
A small change in the translation of the for-loop is proposed. A small change in the translation of the for-loop is proposed.
The statement The statement
for VAR1 in EXPR1: for VAR1 in EXPR1:
BLOCK1 BLOCK1
else:
BLOCK2
will be translated as follows: will be translated as follows:
itr = iter(EXPR1) itr = iter(EXPR1)
arg = None arg = None
brk = False
while True: while True:
try: try:
VAR1 = next(itr, arg) VAR1 = next(itr, arg)
except StopIteration: except StopIteration:
brk = True
break break
arg = None arg = None
BLOCK1 BLOCK1
if brk:
BLOCK2
(However, 'itr' and 'arg' are hidden from the user, their scope (However, the variables 'itr' etc. are not user-visible and the
ends when the while-loop is exited, and they are not shared with built-in names used cannot be overridden by the user.)
nested or outer for-loops, and the user cannot override the
built-ins referenced.)
I'm leaving the translation of an else-clause up to the reader;
note that you can't simply affix the else-clause to the while-loop
since it is always broken out.
Specification: the Extended 'continue' Statement Specification: the Extended 'continue' Statement
@ -180,7 +150,7 @@ Specification: the Extended 'continue' Statement
is legal and is translated into is legal and is translated into
arg = ContinueIteration(EXPR2) arg = EXPR2
continue continue
(Where 'arg' references the corresponding hidden variable from the (Where 'arg' references the corresponding hidden variable from the
@ -195,21 +165,28 @@ Specification: the Anonymous Block Statement
block EXPR1 as VAR1: block EXPR1 as VAR1:
BLOCK1 BLOCK1
else:
BLOCK2
Here, 'block' and 'as' are new keywords; EXPR1 is an arbitrary Here, 'block' and 'as' are new keywords; EXPR1 is an arbitrary
expression (but not an expression-list) and VAR1 is an arbitrary expression (but not an expression-list) and VAR1 is an arbitrary
assignment target (which may be a comma-separated list). assignment target (which may be a comma-separated list).
The "as VAR1" part is optional; if omitted, the assignment to VAR1 The "as VAR1" part is optional; if omitted, the assignments to
in the translation below is omitted (but the next() call is not!). VAR1 in the translation below are omitted (but the expressions
assigned are still evaluated!).
The choice of the 'block' keyword is contentious; it has even been The choice of the 'block' keyword is contentious; many
proposed not to use a keyword at all. PEP 310 uses 'with' for alternatives have been proposed, including not to use a keyword at
similar semantics, but I would like to reserve that for a all (which I actually like). PEP 310 uses 'with' for similar
with-statement similar to the one found in Pascal and VB. To semantics, but I would like to reserve that for a with-statement
sidestep this issue momentarily I'm using 'block' until we can similar to the one found in Pascal and VB. (Though I just found
agree on a keyword. (I just found that the C# designers don't that the C# designers don't like 'with' [2], and I have to agree
like 'with' [2].) with their reasoning.) To sidestep this issue momentarily I'm
using 'block' until we can agree on the right keyword, if any.
Note that the 'as' keyword is not contentious (it will finally be
elevated to proper keyword status).
Note that it is left in the middle whether a block-statement Note that it is left in the middle whether a block-statement
represents a loop or not; this is up to the iterator, but in the represents a loop or not; this is up to the iterator, but in the
@ -218,61 +195,66 @@ Specification: the Anonymous Block Statement
The translation is subtly different from the translation of a The translation is subtly different from the translation of a
for-loop: iter() is not called, so EXPR1 should already be an for-loop: iter() is not called, so EXPR1 should already be an
iterator (not just an iterable); and the iterator is guaranteed to iterator (not just an iterable); and the iterator is guaranteed to
be exhausted when the block-statement is left: be notified when the block-statement is left, regardless if this
is due to a break, return or exception:
itr = EXPR1 itr = EXPR1 # The iterator
val = arg = None ret = False # True if a return statement is active
ret = False val = None # Return value, if ret == True
arg = None # Argument to __next__() (value from continue)
exc = None # sys.exc_info() tuple if an exception is active
while True: while True:
try: try:
VAR1 = next(itr, arg) if exc:
ext = getattr(itr, "__exit__", None)
if ext is not None:
VAR1 = ext(*exc) # May re-raise *exc
else:
raise *exc # Well, the moral equivalent :-)
else:
VAR1 = next(itr, arg) # May raise StopIteration
except StopIteration: except StopIteration:
if ret: if ret:
return val return val
if val is not None:
raise val
break break
try: try:
val = arg = None
ret = False ret = False
val = arg = exc = None
BLOCK1 BLOCK1
except Exception, val: except:
arg = StopIteration() exc = sys.exc_info()
(Again, 'itr' etc. are hidden, and the user cannot override the (Again, the variables and built-ins are hidden from the user.)
built-ins.)
The "raise val" translation is inexact; this is supposed to
re-raise the exact exception that was raised inside BLOCK1, with
the same traceback. We can't use a bare raise-statement because
we've just caught StopIteration.
Inside BLOCK1, the following special translations apply: Inside BLOCK1, the following special translations apply:
- "continue" and "continue EXPR2" are always legal; the latter is - "continue" and "continue EXPR2" are always legal; the latter is
translated as shown earlier: translated as shown earlier:
arg = ContinueIteration(EXPR2) arg = EXPR2
continue continue
- "break" is always legal; it is translated into: - "break" is always legal; it is translated into:
arg = StopIteration() exc = (StopIteration,)
continue continue
- "return EXPR3" is only legal when the block-statement is - "return EXPR3" is only legal when the block-statement is
contained in a function definition; it is translated into: contained in a function definition; it is translated into:
val = EXPR3 exc = (StopIteration,)
ret = True ret = True
arg = StopIteration() val = EXPR3
continue continue
The net effect is that break, continue and return behave much the The net effect is that break, continue and return behave much the
same as if the block-statement were a for-loop, except that the same as if the block-statement were a for-loop, except that the
iterator gets a chance at resource cleanup before the iterator gets a chance at resource cleanup before the
block-statement is left. The iterator also gets a chance if the block-statement is left, through the optional __exit__() method.
block-statement is left through raising an exception. The iterator also gets a chance if the block-statement is left
through raising an exception. If the iterator doesn't have an
__exit__() method, there is no difference with a for-loop (except
that a for-loop calls iter() on EXPR1).
Note that a yield-statement (or a yield-expression, see below) in Note that a yield-statement (or a yield-expression, see below) in
a block-statement is not treated differently. It suspends the a block-statement is not treated differently. It suspends the
@ -287,15 +269,18 @@ Specification: the Anonymous Block Statement
be resumed eventually. be resumed eventually.
I haven't decided yet whether the block-statement should also I haven't decided yet whether the block-statement should also
allow an optional else-clause, like the for-loop. I think it allow an optional else-clause, like the for-loop, but I'm leaning
would be confusing, and emphasize the "loopiness" of the against it. I think it would be confusing, and emphasize the
block-statement, while I want to emphasize its *difference* from a "loopiness" of the block-statement, while I want to emphasize its
for-loop. *difference* from a for-loop. In addition, there are several
possible semantics for an else-clause.
Specification: Generator Exception Handling Specification: Generator Exception Handling
Generators will implement the new __next__() method API, as well Generators will implement the new __next__() method API, as well
as the old argument-less next() method. as the old argument-less next() method which becomes an alias for
calling __next__() without an argument. They will also implement
the new __exit__() method API.
Generators will be allowed to have a yield statement inside a Generators will be allowed to have a yield statement inside a
try-finally statement. try-finally statement.
@ -330,32 +315,41 @@ Specification: Generator Exception Handling
are all illegal. (Some of the edge cases are motivated by the are all illegal. (Some of the edge cases are motivated by the
current legality of "yield 12, 42".) current legality of "yield 12, 42".)
When __next__() is called with a StopIteration instance argument, When __exit__() is called, the generator is resumed but at the
the yield statement that is resumed by the __next__() call will point of the yield-statement or -expression the exception
raise this StopIteration exception. The generator should re-raise represented by the __exit__ argument(s) is raised. The generator
this exception; it should not yield another value. When the may re-raise this exception, raise another exception, or yield
*initial* call to __next__() receives a StopIteration instance another value, execpt that if the exception passed in to
argument, the generator's execution is aborted and the exception __exit__() was StopIteration, it ought to raise StopIteration
is re-raised without passing control to the generator's body. (otherwise the effect would be that a break is turned into
continue, which is unexpected at least). When the *initial* call
resuming the generator is an __exit__() call instead of a
__next__() call, the generator's execution is aborted and the
exception is re-raised without passing control to the generator's
body.
When __next__() is called with a ContinueIteration instance When __next__() is called with an argument that is not None, the
argument, the yield-expression that it resumes will return the yield-expression that it resumes will return the value attribute
value attribute of the argument. If it resumes a yield-statement, of the argument. If it resumes a yield-statement, the value is
the value is ignored. When the *initial* call to __next__() ignored (or should this be considered an error?). When the
receives a ContinueIteration instance argument, the generator's *initial* call to __next__() receives an argument that is not
execution is started normally; the argument's value attribute is None, the generator's execution is started normally; the
ignored. argument's value attribute is ignored (or should this be
considered an error?). When __next__() is called without an
argument or with None as argument, and a yield-expression is
resumed, the yield-expression returns None.
When a generator that has not yet terminated is garbage-collected When a generator that has not yet terminated is garbage-collected
(either through reference counting or by the cyclical garbage (either through reference counting or by the cyclical garbage
collector), its __next__() method is called once with a collector), its __exit__() method is called once with
StopIteration instance argument. Together with the requirement StopIteration as its first argument. Together with the
that __next__() should always re-raise a StopIteration argument, requirement that a generator ought to raise StopIteration when
this guarantees the eventual activation of any finally-clauses __exit__() is called with StopIteration, this guarantees the
that were active when the generator was last suspended. Of eventual activation of any finally-clauses that were active when
course, under certain circumstances the generator may never be the generator was last suspended. Of course, under certain
garbage-collected. This is no different than the guarantees that circumstances the generator may never be garbage-collected. This
are made about finalizers (__del__() methods) of other objects. is no different than the guarantees that are made about finalizers
(__del__() methods) of other objects.
Note: the syntactic extensions to yield make its use very similar Note: the syntactic extensions to yield make its use very similar
to that in Ruby. This is intentional. Do note that in Python the to that in Ruby. This is intentional. Do note that in Python the
@ -367,71 +361,6 @@ Specification: Generator Exception Handling
cases work differently; in Python, you cannot save the block for cases work differently; in Python, you cannot save the block for
later use, and you cannot test whether there is a block or not. later use, and you cannot test whether there is a block or not.
Specification: Alternative __next__() and Generator Exception Handling
The above specification doesn't let the generator handle general
exceptions. If we want that, we could modify the __next__() API
to take either a value or an exception argument, with an
additional flag argument to distinguish between the two. When the
second argument is True, the first must be an Exception instance,
which raised at the point of the resuming yield; otherwise the
first argument is the value that is returned from the
yield-expression (or ignored by a yield-statement). Wrapping a
regular value in a ContinueIteration is then no longer necessary.
The next() built-in would be modified likewise:
def next(itr, arg=None, exc=False):
nxt = getattr(itr, "__next__", None)
if nxt is not None:
return nxt(arg, exc)
if arg is None and not exc:
return itr.next()
raise TypeError("next() with args for old-style iterator")
The translation of a block-statement would become:
itr = EXPR1
arg = val = None
ret = exc = False
while True:
try:
VAR1 = next(itr, arg, exc)
except StopIteration:
if ret:
return val
break
try:
arg = val = None
ret = exc = False
BLOCK1
except Exception, arg:
exc = True
The translation of "continue EXPR2" would become:
arg = EXPR2
continue
The translation of "break" inside a block-statement would become:
arg = StopIteration()
exc = True
continue
The translation of "return EXPR3" inside a block-statement would
become:
val = EXPR3
arg = StopIteration()
ret = exc = True
continue
The translation of a for-loop would be the same as indicated
earlier (inside a for-loop only the translation of "continue
EXPR2" is changed; break and return translate to themselves in
that case).
Loose Ends Loose Ends
These are things that need to be resolved before accepting the These are things that need to be resolved before accepting the
@ -447,17 +376,8 @@ Loose Ends
- Decide on the keyword ('block', 'with', '@', nothing, or - Decide on the keyword ('block', 'with', '@', nothing, or
something else?). something else?).
- Phillip Eby wants a way to pass tracebacks along with
exceptions.
- The translation for the for-loop's else-clause.
- Whether a block-statement should allow an else-clause. - Whether a block-statement should allow an else-clause.
- Which API to use to pass in an exception: itr.__next__(exc),
itr.__next__(exc, True) or itr.__exit__(exc[, traceback]).
Hmm..., perhaps itr.__next__(exc, traceback)?
Comparison to Thunks Comparison to Thunks
Alternative semantics proposed for the block-statement turn the Alternative semantics proposed for the block-statement turn the
@ -626,7 +546,7 @@ Examples
block auto_retry(3, IOError): block auto_retry(3, IOError):
f = urllib.urlopen("http://python.org/peps/pep-0340.html") f = urllib.urlopen("http://python.org/peps/pep-0340.html")
print f.read() print f.read()
5. It is possible to nest blocks and combine templates: 5. It is possible to nest blocks and combine templates: