PEP 463: More updates by Chris Angelico

This commit is contained in:
Yury Selivanov 2014-02-21 23:27:51 -05:00
parent 3baaf22f53
commit 33340f76c2
1 changed files with 84 additions and 113 deletions

View File

@ -41,9 +41,6 @@ support this.
* min(sequence, default=default) - keyword argument in place of
ValueError
* sum(sequence, start=default) - slightly different but can do the
same job
* statistics.mean(data) - no way to handle an empty iterator
@ -131,11 +128,11 @@ This currently works::
The proposal adds this::
lst = [1, 2]
value = lst[2] except IndexError: "No value"
value = (lst[2] except IndexError: "No value")
Specifically, the syntax proposed is::
expr except exception_list: default
(expr except exception_list: default)
where expr, exception_list, and default are all expressions. First,
expr is evaluated. If no exception is raised, its value is the value
@ -146,6 +143,12 @@ result in the corresponding default expression being evaluated and
becoming the value of the expression. As with the statement form of
try/except, non-matching exceptions will propagate upward.
Parentheses are required around the entire expression, unless they
would be completely redundant, according to the same rules as generator
expressions follow. This guarantees correct interpretation of nested
except-expressions, and allows for future expansion of the syntax -
see below on multiple except clauses.
Note that the current proposal does not allow the exception object to
be captured. Where this is needed, the statement form must be used.
(See below for discussion and elaboration on this.)
@ -206,20 +209,39 @@ All forms involving the 'as' capturing clause have been deferred from
this proposal in the interests of simplicity, but are preserved in the
table above as an accurate record of suggestions.
The four forms most supported by this proposal are, in order::
Open Issues
===========
value = (expr except Exception: default)
value = (expr except Exception -> default)
value = (expr except Exception pass default)
value = (expr except Exception then default)
Parentheses around the entire expression
----------------------------------------
All four maintain left-to-right evaluation order: first the base expression,
then the exception list, and lastly the default. This is important, as the
expressions are evaluated lazily. By comparison, several of the ad-hoc
alternatives listed above must (by the nature of functions) evaluate their
default values eagerly. The preferred form, using the colon, parallels
try/except by using "except exception_list:", and parallels lambda by having
"keyword name_list: subexpression". Using the arrow introduces a token many
programmers will not be familiar with, and which currently has no similar
meaning, but is otherwise quite readable. The English word "pass" has a
vaguely similar meaning (consider the common usage "pass by value/reference"
for function arguments), and "pass" is already a keyword, but as its meaning
is distinctly unrelated, this may cause confusion. Using "then" makes sense
in English, but this introduces a new keyword to the language - albeit one
not in common use, but a new keyword all the same.
Generator expressions require parentheses, unless they would be
strictly redundant. Ambiguities with except expressions could be
resolved in the same way, forcing nested except-in-except trees to be
correctly parenthesized and requiring that the outer expression be
clearly delineated. `Steven D'Aprano elaborates on the issue.`__
Left to right evaluation order is extremely important to readability, as it
parallels the order most expressions are evaluated. Alternatives such as::
__ https://mail.python.org/pipermail/python-ideas/2014-February/025647.html
value = (expr except default if Exception)
break this, by first evaluating the two ends, and then coming to the middle;
while this may not seem terrible (as the exception list will usually be a
constant), it does add to the confusion when multiple clauses meet, either
with multiple except/if or with the existing if/else, or a combination.
Using the preferred order, subexpressions will always be evaluated from
left to right, no matter how the syntax is nested.
Example usage
@ -236,7 +258,7 @@ Many of these patterns are extremely common.
Retrieve an argument, defaulting to None::
cond = args[1] except IndexError: None
cond = (args[1] except IndexError: None)
# Lib/pdb.py:803:
try:
@ -246,7 +268,7 @@ Retrieve an argument, defaulting to None::
Fetch information from the system if available::
pwd = os.getcwd() except OSError: None
pwd = (os.getcwd() except OSError: None)
# Lib/tkinter/filedialog.py:210:
try:
@ -256,7 +278,7 @@ Fetch information from the system if available::
Attempt a translation, falling back on the original::
e.widget = self._nametowidget(W) except KeyError: W
e.widget = (self._nametowidget(W) except KeyError: W)
# Lib/tkinter/__init__.py:1222:
try:
@ -267,7 +289,7 @@ Attempt a translation, falling back on the original::
Read from an iterator, continuing with blank lines once it's
exhausted::
line = readline() except StopIteration: ''
line = (readline() except StopIteration: '')
# Lib/lib2to3/pgen2/tokenize.py:370:
try:
@ -280,7 +302,7 @@ this particular example could be taken further, turning a series of
separate assignments into a single large dict initialization::
# sys.abiflags may not be defined on all platforms.
_CONFIG_VARS['abiflags'] = sys.abiflags except AttributeError: ''
_CONFIG_VARS['abiflags'] = (sys.abiflags except AttributeError: '')
# Lib/sysconfig.py:529:
try:
@ -292,7 +314,7 @@ separate assignments into a single large dict initialization::
Retrieve an indexed item, defaulting to None (similar to dict.get)::
def getNamedItem(self, name):
return self._attrs[name] except KeyError: None
return (self._attrs[name] except KeyError: None)
# Lib/xml/dom/minidom.py:573:
def getNamedItem(self, name):
@ -303,8 +325,8 @@ Retrieve an indexed item, defaulting to None (similar to dict.get)::
Translate numbers to names, falling back on the numbers::
g = grp.getgrnam(tarinfo.gname)[2] except KeyError: tarinfo.gid
u = pwd.getpwnam(tarinfo.uname)[2] except KeyError: tarinfo.uid
g = (grp.getgrnam(tarinfo.gname)[2] except KeyError: tarinfo.gid)
u = (pwd.getpwnam(tarinfo.uname)[2] except KeyError: tarinfo.uid)
# Lib/tarfile.py:2198:
try:
@ -319,7 +341,7 @@ Translate numbers to names, falling back on the numbers::
Perform some lengthy calculations in EAFP mode, handling division by
zero as a sort of sticky NaN::
value = calculate(x) except ZeroDivisionError: float("nan")
value = (calculate(x) except ZeroDivisionError: float("nan"))
try:
value = calculate(x)
@ -328,38 +350,13 @@ zero as a sort of sticky NaN::
Calculate the mean of a series of numbers, falling back on zero::
value = statistics.mean(lst) except statistics.StatisticsError: 0
value = (statistics.mean(lst) except statistics.StatisticsError: 0)
try:
value = statistics.mean(lst)
except statistics.StatisticsError:
value = 0
Retrieving a message from either a cache or the internet, with auth
check::
logging.info("Message shown to user: %s",((cache[k]
except LookupError:
(backend.read(k) except OSError: 'Resource not available')
)
if check_permission(k) else 'Access denied'
) except BaseException: "This is like a bare except clause")
try:
if check_permission(k):
try:
_ = cache[k]
except LookupError:
try:
_ = backend.read(k)
except OSError:
_ = 'Resource not available'
else:
_ = 'Access denied'
except BaseException:
_ = "This is like a bare except clause"
logging.info("Message shown to user: %s", _)
Looking up objects in a sparse list of overrides::
(overrides[x] or default except IndexError: default).ping()
@ -419,11 +416,19 @@ Becomes::
status.append('%s:%d' % self.addr except TypeError: repr(self.addr))
In each case, the narrowed scope of the try/except ensures that an unexpected
exception (for instance, AttributeError if "append" were misspelled) does not
get caught by the same handler. This is sufficiently unlikely to be reason
to break the call out into a separate line (as per the five line example
above), but it is a small benefit gained as a side-effect of the conversion.
Comparisons with other languages
================================
(With thanks to Andrew Barnert for compiling this section.)
(With thanks to Andrew Barnert for compiling this section. Note that the
examples given here do not reflect the current version of the proposal,
and need to be edited.)
`Ruby's`__ "begin…rescue…rescue…else…ensure…end" is an expression
(potentially with statements inside it). It has the equivalent of an "as"
@ -611,23 +616,18 @@ An examination of use-cases shows that this is not needed as often as
it would be with the statement form, and as its syntax is a point on
which consensus has not been reached, the entire feature is deferred.
In order to ensure compatibility with future versions, ensure that any
consecutive except operators are parenthesized to guarantee the
interpretation you expect.
Multiple 'except' keywords can be used, and they will all catch
exceptions raised in the original expression (only)::
# Will catch any of the listed exceptions thrown by expr;
# any exception thrown by a default expression will propagate.
value = (expr
except Exception1 [as e]: default1
except Exception2 [as e]: default2
# ... except ExceptionN [as e]: defaultN
except Exception1: default1
except Exception2: default2
# ... except ExceptionN: defaultN
)
Using parentheses to force an alternative interpretation works as
expected::
Currently, one of the following forms must be used::
# Will catch an Exception2 thrown by either expr or default1
value = (
@ -639,69 +639,35 @@ expected::
(default1 except Exception2: default2)
)
This last form is confusing and should be discouraged by PEP 8, but it
is syntactically legal: you can put any sort of expression inside a
ternary-except; ternary-except is an expression; therefore you can put
a ternary-except inside a ternary-except.
Open question: Where there are multiple except clauses, should they be
separated by commas? It may be easier for the parser, that way::
value = (expr
except Exception1 [as e]: default1,
except Exception2 [as e]: default2,
# ... except ExceptionN [as e]: defaultN,
)
with an optional comma after the last, as per tuple rules. Downside:
Omitting the comma would be syntactically valid, and would have almost
identical semantics, but would nest the entire preceding expression in
its exception catching rig - a matching exception raised in the
default clause would be caught by the subsequent except clause. As
this difference is so subtle, it runs the risk of being a major bug
magnet.
As a mitigation of this risk, this form::
value = expr except Exception1: default1 except Exception2: default2
could be syntactically forbidden, and parentheses required if the
programmer actually wants that behaviour::
value = (expr except Exception1: default1) except Exception2: default2
This would prevent the accidental omission of a comma from changing
the expression's meaning.
Listing multiple exception clauses without parentheses is a syntax error
(see above), and so a future version of Python is free to add this feature
without breaking any existing code.
Capturing the exception object
------------------------------
In a try/except block, the use of 'as' to capture the exception object
creates a local name binding, and implicitly deletes that binding in a
finally clause. As 'finally' is not a part of this proposal (see
below), this makes it tricky to describe; also, this use of 'as' gives
a way to create a name binding in an expression context. Should the
default clause have an inner scope in which the name exists, shadowing
anything of the same name elsewhere? Should it behave the same way the
statement try/except does, and unbind the name? Should it bind the
name and leave it bound? (Almost certainly not; this behaviour was
changed in Python 3 for good reason.)
creates a local name binding, and implicitly deletes that binding (to
avoid creating a reference loop) in a finally clause. In an expression
context, this makes little sense, and a proper sub-scope would be
required to safely capture the exception object - something akin to the
way a list comprehension is handled. However, CPython currently
implements a comprehension's subscope with a nested function call, which
has consequences in some contexts such as class definitions, and is
therefore unsuitable for this proposal. Should there be, in future, a
way to create a true subscope (which could simplify comprehensions,
except expressions, with blocks, and possibly more), then this proposal
could be revived; until then, its loss is not a great one, as the simple
exception handling that is well suited to the expression notation used
here is generally concerned only with the type of the exception, and not
its value - further analysis below.
Additionally, this syntax would allow a convenient way to capture
This syntax would, admittedly, allow a convenient way to capture
exceptions in interactive Python; returned values are captured by "_",
but exceptions currently are not. This could be spelled::
>>> expr except Exception as e: e
(The inner scope idea is tempting, but currently CPython handles list
comprehensions with a nested function call, as this is considered
easier. It may be of value to simplify both comprehensions and except
expressions, but that is a completely separate proposal to this PEP;
alternatively, it may be better to stick with what's known to
work. `Nick Coghlan elaborates.`__)
__ https://mail.python.org/pipermail/python-ideas/2014-February/025702.html
>>> (expr except Exception as e: e)
An examination of the Python standard library shows that, while the use
of 'as' is fairly common (occurring in roughly one except clause in five),
@ -732,6 +698,7 @@ Rejected sub-proposals
finally clause
--------------
The statement form try... finally or try... except... finally has no
logical corresponding expression form. Therefore the finally keyword
is not a part of this proposal, in any way.
@ -813,6 +780,10 @@ course, be parenthesized if desired, as can the default::
value = (expr) except ExceptionType: (default)
As the entire expression is now required to be in parentheses (which had not
been decided at the time when this was debated), there is less need to
delineate this section, and in many cases it would be redundant.
Short-hand for "except: pass"
-----------------------------