Add PEP 463 by Chris Angelico.
This commit is contained in:
parent
4f90b7adc9
commit
c87a214fc0
|
@ -0,0 +1,436 @@
|
|||
PEP: 463
|
||||
Title: Exception-catching expressions
|
||||
Version: $Revision$
|
||||
Last-Modified: $Date$
|
||||
Author: Chris Angelico <rosuav@gmail.com>
|
||||
Status: Draft
|
||||
Type: Standards Track
|
||||
Content-Type: text/x-rst
|
||||
Created: 15-Feb-2014
|
||||
Python-Version: 3.5
|
||||
Post-History: 16-Feb-2014
|
||||
|
||||
|
||||
Abstract
|
||||
========
|
||||
|
||||
Just as PEP 308 introduced a means of value-based conditions in an
|
||||
expression, this system allows exception-based conditions to be used
|
||||
as part of an expression.
|
||||
|
||||
|
||||
Motivation
|
||||
==========
|
||||
|
||||
A number of functions and methods have parameters which will cause
|
||||
them to return a specified value instead of raising an exception. The
|
||||
current system is ad-hoc and inconsistent, and requires that each
|
||||
function be individually written to have this functionality; not all
|
||||
support this.
|
||||
|
||||
* dict.get(key, default) - second positional argument in place of
|
||||
KeyError
|
||||
|
||||
* next(iter, default) - second positional argument in place of
|
||||
StopIteration
|
||||
|
||||
* list.pop() - no way to return a default
|
||||
|
||||
* seq[index] - no way to handle a bounds error
|
||||
|
||||
* 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
|
||||
|
||||
Additionally, this syntax would 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
|
||||
|
||||
|
||||
Rationale
|
||||
=========
|
||||
|
||||
The current system requires that a function author predict the need
|
||||
for a default, and implement support for it. If this is not done, a
|
||||
full try/except block is needed.
|
||||
|
||||
Since try/except is a statement, it is impossible to catch exceptions
|
||||
in the middle of an expression. Just as if/else does for conditionals
|
||||
and lambda does for function definitions, so does this allow exception
|
||||
catching in an expression context.
|
||||
|
||||
This provides a clean and consistent way for a function to provide a
|
||||
default: it simply raises an appropriate exception, and the caller
|
||||
catches it.
|
||||
|
||||
There's no convenient way to write a helper function to do this; the
|
||||
nearest is something ugly using either lambda::
|
||||
|
||||
def except_(expression, exception_list, default):
|
||||
try:
|
||||
return expression()
|
||||
except exception_list as e:
|
||||
return default(e)
|
||||
value = except_(lambda: 1/x, ZeroDivisionError, lambda e: float("nan"))
|
||||
|
||||
which is clunky, and unable to handle multiple exception clauses; or
|
||||
eval::
|
||||
|
||||
def except_(expression, exception_list, default):
|
||||
try:
|
||||
return eval(expression, globals_of_caller(), locals_of_caller())
|
||||
except exception_list as exc:
|
||||
l = locals_of_caller().copy()
|
||||
l['exc'] = exc
|
||||
return eval(default, globals_of_caller(), l)
|
||||
|
||||
def globals_of_caller():
|
||||
return sys._getframe(2).f_globals
|
||||
|
||||
def locals_of_caller():
|
||||
return sys._getframe(2).f_locals
|
||||
|
||||
value = except_("""1/x""",ZeroDivisionError,""" "Can't divide by zero" """)
|
||||
|
||||
which is even clunkier, and relies on implementation-dependent hacks.
|
||||
(Writing globals_of_caller() and locals_of_caller() for interpreters
|
||||
other than CPython is left as an exercise for the reader.)
|
||||
|
||||
Raymond Hettinger `expresses`__ a desire for such a consistent
|
||||
API. Something similar has been `requested`__ `multiple`__ `times`__
|
||||
in the past.
|
||||
|
||||
__ https://mail.python.org/pipermail/python-ideas/2014-February/025443.html
|
||||
__ https://mail.python.org/pipermail/python-ideas/2013-March/019760.html
|
||||
__ https://mail.python.org/pipermail/python-ideas/2009-August/005441.html
|
||||
__ https://mail.python.org/pipermail/python-ideas/2008-August/001801.html
|
||||
|
||||
|
||||
Proposal
|
||||
========
|
||||
|
||||
Just as the 'or' operator and the three part 'if-else' expression give
|
||||
short circuiting methods of catching a falsy value and replacing it,
|
||||
this syntax gives a short-circuiting method of catching an exception
|
||||
and replacing it.
|
||||
|
||||
This currently works::
|
||||
|
||||
lst = [1, 2, None, 3]
|
||||
value = lst[2] or "No value"
|
||||
|
||||
The proposal adds this::
|
||||
|
||||
lst = [1, 2]
|
||||
value = lst[2] except IndexError: "No value"
|
||||
|
||||
Specifically, the syntax proposed is::
|
||||
|
||||
expr except exc [as e]: default [except exc2 [as e]: default2] ...
|
||||
|
||||
where expr, exc, and default are all expressions. First, expr is
|
||||
evaluated. If no exception is raised, its value is the value of the
|
||||
overall expression. If any exception is raised, exc is evaluated, and
|
||||
should result in either a type or a tuple, just as with the statement
|
||||
form of try/except. Any matching exception will result in the
|
||||
corresponding default expression being evaluated and becoming the
|
||||
value of the expression. As with the statement form of try/ except,
|
||||
subsequent except clauses will be checked if the first one does not
|
||||
match, and if none match, the raised exception will propagate upward.
|
||||
Also as with the try/except statement, the keyword 'as' can be used to
|
||||
bind the exception object to a local name.
|
||||
|
||||
Omitting the exception list should be legal, just as with the
|
||||
statement form of try/except, though this should of course be
|
||||
discouraged by PEP 8.
|
||||
|
||||
The exception object can be captured just as in a normal try/except
|
||||
block::
|
||||
|
||||
# Return the next yielded or returned value from a generator
|
||||
value = next(it) except StopIteration as e: e.args[0]
|
||||
|
||||
This is effectively equivalent to::
|
||||
|
||||
try:
|
||||
_ = next(it)
|
||||
except StopIteration as e:
|
||||
_ = e.args[0]
|
||||
value = _
|
||||
|
||||
This ternary operator would be between lambda and if/else in
|
||||
precedence.
|
||||
|
||||
|
||||
Chaining
|
||||
--------
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
Using parentheses to force an alternative interpretation works as
|
||||
expected::
|
||||
|
||||
# Will catch an Exception2 thrown by either expr or default1
|
||||
value = (
|
||||
(expr except Exception1: default1)
|
||||
except Exception2: default2
|
||||
)
|
||||
# Will catch an Exception2 thrown by default1 only
|
||||
value = (expr except Exception1:
|
||||
(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.
|
||||
|
||||
|
||||
Alternative Proposals
|
||||
=====================
|
||||
|
||||
Discussion on python-ideas brought up the following syntax suggestions::
|
||||
|
||||
value = expr except default if Exception [as e]
|
||||
value = expr except default for Exception [as e]
|
||||
value = expr except default from Exception [as e]
|
||||
value = expr except Exception [as e] return default
|
||||
value = expr except (Exception [as e]: default)
|
||||
value = expr except Exception [as e] try default
|
||||
value = expr except Exception [as e] continue with default
|
||||
value = default except Exception [as e] else expr
|
||||
value = try expr except Exception [as e]: default
|
||||
value = expr except Exception [as e] pass default
|
||||
|
||||
It has also been suggested that a new keyword be created, rather than
|
||||
reusing an existing one. Such proposals fall into the same structure
|
||||
as the last form, but with a different keyword in place of 'pass'.
|
||||
Suggestions include 'then', 'when', and 'use'.
|
||||
|
||||
Additionally, the following has been suggested as a similar
|
||||
short-hand, though not technically an expression::
|
||||
|
||||
statement except Exception: pass
|
||||
|
||||
try:
|
||||
statement
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
Open Issues
|
||||
===========
|
||||
|
||||
Commas between multiple except clauses
|
||||
--------------------------------------
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Parentheses around the entire expression
|
||||
----------------------------------------
|
||||
|
||||
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.`__
|
||||
|
||||
__ https://mail.python.org/pipermail/python-ideas/2014-February/025647.html
|
||||
|
||||
|
||||
Parentheses around the except clauses
|
||||
-------------------------------------
|
||||
|
||||
Should it be legal to parenthesize the except clauses, separately from
|
||||
the expression that could raise? Example::
|
||||
|
||||
value = expr (
|
||||
except Exception1 [as e]: default1
|
||||
except Exception2 [as e]: default2
|
||||
# ... except ExceptionN [as e]: defaultN
|
||||
)
|
||||
|
||||
|
||||
Scope of default expressions and 'as'
|
||||
-------------------------------------
|
||||
|
||||
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.)
|
||||
|
||||
(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
|
||||
|
||||
|
||||
Example usage
|
||||
=============
|
||||
|
||||
For each example, an approximately-equivalent statement form is given,
|
||||
to show how the expression will be parsed. These are not always
|
||||
strictly equivalent, but will accomplish the same purpose. It is NOT
|
||||
safe for the interpreter to translate one into the other.
|
||||
|
||||
Perform some lengthy calculations in EAFP mode, handling division by
|
||||
zero as a sort of sticky NaN::
|
||||
|
||||
value = calculate(x) except ZeroDivisionError: float("nan")
|
||||
|
||||
try:
|
||||
value = calculate(x)
|
||||
except ZeroDivisionError:
|
||||
value = float("nan")
|
||||
|
||||
Retrieving from a generator, either the next yielded value or the
|
||||
returned, and coping with the absence of such a return value::
|
||||
|
||||
value = (next(it)
|
||||
except StopIteration as e:
|
||||
(e.args[0] except IndexError: None)
|
||||
)
|
||||
|
||||
try:
|
||||
value = next(it)
|
||||
except StopIteration as e:
|
||||
try:
|
||||
value = e.args[0]
|
||||
except IndexError:
|
||||
value = None
|
||||
|
||||
Calculate the mean of a series of numbers, falling back on zero::
|
||||
|
||||
value = statistics.mean(lst) except statistics.StatisticsError: 0
|
||||
|
||||
try:
|
||||
value = statistics.mean(lst)
|
||||
except statistics.StatisticsError:
|
||||
value = 0
|
||||
|
||||
Set a PyGTK label to a human-readable result from fetching a URL::
|
||||
|
||||
display.set_text(
|
||||
urllib.request.urlopen(url)
|
||||
except urllib.error.HTTPError as e: "Error %d: %s"%(x.getcode(), x.msg)
|
||||
except (ValueError, urllib.error.URLError) as e: "Invalid URL: "+str(e)
|
||||
)
|
||||
|
||||
try:
|
||||
display.set_text(urllib.request.urlopen(url))
|
||||
except urllib.error.HTTPError as e:
|
||||
display.set_text("Error %d: %s"%(x.getcode(), x.msg))
|
||||
except (ValueError, urllib.error.URLError) as e:
|
||||
display.set_text("Invalid URL: "+str(e))
|
||||
|
||||
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: "I'm an idiot and using 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:
|
||||
_ = "I'm an idiot and using 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()
|
||||
|
||||
try:
|
||||
(overrides[x] or default).ping()
|
||||
except IndexError:
|
||||
default.ping()
|
||||
|
||||
|
||||
Rejected sub-proposals
|
||||
======================
|
||||
|
||||
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.
|
||||
|
||||
|
||||
Copyright
|
||||
=========
|
||||
|
||||
This document has been placed in the public domain.
|
||||
|
||||
|
||||
|
||||
..
|
||||
Local Variables:
|
||||
mode: indented-text
|
||||
indent-tabs-mode: nil
|
||||
sentence-end-double-space: t
|
||||
fill-column: 70
|
||||
coding: utf-8
|
||||
End:
|
Loading…
Reference in New Issue