PEP 463: Exception-catching expressions
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, 21-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 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. With some situations, an LBYL technique can be used (checking if some sequence has enough length before indexing into it, for instance). This is not safe in all cases, but as it is often convenient, programmers will be tempted to sacrifice the safety of EAFP in favour of the notational brevity of LBYL. Additionally, some LBYL techniques (eg involving getattr with three arguments) warp the code into looking like literal strings rather than attribute lookup, which can impact readability. A convenient EAFP notation solves all of this. 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: return default() value = except_(lambda: 1/x, ZeroDivisionError, lambda: 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 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 of the overall expression. If any exception is raised, exception_list 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, non-matching exceptions will propagate upward. 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.) This ternary operator would be between lambda and if/else in precedence. Consider this example of a two-level cache:: for key in sequence: x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key))) # do something with x This cannot be rewritten as:: x = lvl1.get(key, lvl2.get(key, f(key))) which, despite being shorter, defeats the purpose of the cache, as it must calculate a default value to pass to get(). The .get() version calculates backwards; the exception-testing version calculates forwards, as would be expected. The nearest useful equivalent would be:: x = lvl1.get(key) or lvl2.get(key) or f(key) which depends on the values being nonzero, as well as depending on the cache object supporting this functionality. 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 default # Catches anything value = expr except(Exception) default # Catches only the named type(s) value = default if expr raise Exception value = expr or else default if Exception value = 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'. Also, in the context of the "default if expr raise Exception" proposal, it was suggested that a new keyword "raises" be used. 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. Open Issues =========== 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 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. A number of these examples are taken directly from the Python standard library, with file names and line numbers correct as of early Feb 2014. Many of these patterns are extremely common. Retrieve an argument, defaulting to None:: cond = args[1] except IndexError: None # Lib/pdb.py:803: try: cond = args[1] except IndexError: cond = None Fetch information from the system if available:: pwd = os.getcwd() except OSError: None # Lib/tkinter/filedialog.py:210: try: pwd = os.getcwd() except OSError: pwd = None Attempt a translation, falling back on the original:: e.widget = self._nametowidget(W) except KeyError: W # Lib/tkinter/__init__.py:1222: try: e.widget = self._nametowidget(W) except KeyError: e.widget = W Read from an iterator, continuing with blank lines once it's exhausted:: line = readline() except StopIteration: '' # Lib/lib2to3/pgen2/tokenize.py:370: try: line = readline() except StopIteration: line = '' Retrieve platform-specific information (note the DRY improvement); 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: '' # Lib/sysconfig.py:529: try: _CONFIG_VARS['abiflags'] = sys.abiflags except AttributeError: # sys.abiflags may not be defined on all platforms. _CONFIG_VARS['abiflags'] = '' Retrieve an indexed item, defaulting to None (similar to dict.get):: def getNamedItem(self, name): return self._attrs[name] except KeyError: None # Lib/xml/dom/minidom.py:573: def getNamedItem(self, name): try: return self._attrs[name] except KeyError: return None 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 # Lib/tarfile.py:2198: try: g = grp.getgrnam(tarinfo.gname)[2] except KeyError: g = tarinfo.gid try: u = pwd.getpwnam(tarinfo.uname)[2] except KeyError: u = tarinfo.uid 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") 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 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() try: (overrides[x] or default).ping() except IndexError: default.ping() Narrowing of exception-catching scope ------------------------------------- The following examples, taken directly from Python's standard library, demonstrate how the scope of the try/except can be conveniently narrowed. To do this with the statement form of try/except would require a temporary variable, but it's far cleaner as an expression. Lib/ipaddress.py:343:: try: ips.append(ip.ip) except AttributeError: ips.append(ip.network_address) Becomes:: ips.append(ip.ip except AttributeError: ip.network_address) The expression form is nearly equivalent to this:: try: _ = ip.ip except AttributeError: _ = ip.network_address ips.append(_) Lib/tempfile.py:130:: try: dirlist.append(_os.getcwd()) except (AttributeError, OSError): dirlist.append(_os.curdir) Becomes:: dirlist.append(_os.getcwd() except (AttributeError, OSError): _os.curdir) Lib/asyncore.py:264:: try: status.append('%s:%d' % self.addr) except TypeError: status.append(repr(self.addr)) Becomes:: status.append('%s:%d' % self.addr except TypeError: repr(self.addr)) Comparisons with other languages ================================ (With thanks to Andrew Barnert for compiling this section.) `Ruby's`__ "begin…rescue…rescue…else…ensure…end" is an expression (potentially with statements inside it). It has the equivalent of an "as" clause, and the equivalent of bare except. And it uses no punctuation or keyword between the bare except/exception class/exception class with as clause and the value. (And yes, it's ambiguous unless you understand Ruby's statement/expression rules.) __ http://www.skorks.com/2009/09/ruby-exceptions-and-exception-handling/ :: x = begin computation() rescue MyException => e default(e) end; x = begin computation() rescue MyException default() end; x = begin computation() rescue default() end; x = begin computation() rescue MyException default() rescue OtherException other() end; In terms of this PEP:: x = computation() except MyException as e default(e) x = computation() except MyException default(e) x = computation() except default(e) x = computation() except MyException default() except OtherException other() `Erlang`__ has a try expression that looks like this:: __ http://erlang.org/doc/reference_manual/expressions.html#id79284 x = try computation() catch MyException:e -> default(e) end; x = try computation() catch MyException:e -> default(e); OtherException:e -> other(e) end; The class and "as" name are mandatory, but you can use "_" for either. There's also an optional "when" guard on each, and a "throw" clause that you can catch, which I won't get into. To handle multiple exceptions, you just separate the clauses with semicolons, which I guess would map to commas in Python. So:: x = try computation() except MyException as e -> default(e) x = try computation() except MyException as e -> default(e), OtherException as e->other_default(e) Erlang also has a "catch" expression, which, despite using the same keyword, is completely different, and you don't want to know about it. The ML family has two different ways of dealing with this, "handle" and "try"; the difference between the two is that "try" pattern-matches the exception, which gives you the effect of multiple except clauses and as clauses. In either form, the handler clause is punctuated by "=>" in some dialects, "->" in others. To avoid confusion, I'll write the function calls in Python style. Here's `SML's`__ "handle":: __ http://www.cs.cmu.edu/~rwh/introsml/core/exceptions.htm let x = computation() handle MyException => default();; Here's `OCaml's`__ "try":: __ http://www2.lib.uchicago.edu/keith/ocaml-class/exceptions.html let x = try computation() with MyException explanation -> default(explanation);; let x = try computation() with MyException(e) -> default(e) | MyOtherException() -> other_default() | (e) -> fallback(e);; In terms of this PEP, these would be something like:: x = computation() except MyException => default() x = try computation() except MyException e -> default() x = (try computation() except MyException as e -> default(e) except MyOtherException -> other_default() except BaseException as e -> fallback(e)) Many ML-inspired but not-directly-related languages from academia mix things up, usually using more keywords and fewer symbols. So, the `Oz`__ would map to Python as:: __ http://mozart.github.io/mozart-v1/doc-1.4.0/tutorial/node5.html x = try computation() catch MyException as e then default(e) Many Lisp-derived languages, like `Clojure,`__ implement try/catch as special forms (if you don't know what that means, think function-like macros), so you write, effectively:: __ http://clojure.org/special_forms#Special%20Forms--(try%20expr*%20catch-claus...) try(computation(), catch(MyException, explanation, default(explanation))) try(computation(), catch(MyException, explanation, default(explanation)), catch(MyOtherException, explanation, other_default(explanation))) In Common Lisp, this is done with a slightly clunkier `"handler-case" macro,`__ but the basic idea is the same. __ http://clhs.lisp.se/Body/m_hand_1.htm The Lisp style is, surprisingly, used by some languages that don't have macros, like Lua, where `xpcall`__ takes functions. Writing lambdas Python-style instead of Lua-style:: __ http://www.gammon.com.au/scripts/doc.php?lua=xpcall x = xpcall(lambda: expression(), lambda e: default(e)) This actually returns (true, expression()) or (false, default(e)), but I think we can ignore that part. Haskell is actually similar to Lua here (except that it's all done with monads, of course):: x = do catch(lambda: expression(), lambda e: default(e)) You can write a pattern matching expression within the function to decide what to do with it; catching and re-raising exceptions you don't want is cheap enough to be idiomatic. But Haskell infixing makes this nicer:: x = do expression() `catch` lambda: default() x = do expression() `catch` lambda e: default(e) And that makes the parallel between the lambda colon and the except colon in the proposal much more obvious:: x = expression() except Exception: default() x = expression() except Exception as e: default(e) `Tcl`__ has the other half of Lua's xpcall; catch is a function which returns true if an exception was caught, false otherwise, and you get the value out in other ways. And it's all built around the the implicit quote-and-exec that everything in Tcl is based on, making it even harder to describe in Python terms than Lisp macros, but something like:: __ http://wiki.tcl.tk/902 if {[ catch("computation()") "explanation"]} { default(explanation) } `Smalltalk`__ is also somewhat hard to map to Python. The basic version would be:: __ http://smalltalk.gnu.org/wiki/exceptions x := computation() on:MyException do:default() … but that's basically Smalltalk's passing-arguments-with-colons syntax, not its exception-handling syntax. Deferred sub-proposals ====================== Multiple except clauses ----------------------- 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 ) 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. 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. 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.) 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
(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 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), it is extremely *uncommon* in the cases which could logically be converted into the expression form. Its few uses can simply be left unchanged. Consequently, in the interests of simplicity, the 'as' clause is not included in this proposal. A subsequent Python version can add this without breaking any existing code, as 'as' is already a keyword. One example where this could possibly be useful is Lib/imaplib.py:568:: try: typ, dat = self._simple_command('LOGOUT') except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]] This could become:: typ, dat = (self._simple_command('LOGOUT') except BaseException as e: ('NO', '%s: %s' % (type(e), e))) Or perhaps some other variation. This is hardly the most compelling use-case, but an intelligent look at this code could tidy it up significantly. In the absence of further examples showing any need of the exception object, I have opted to defer indefinitely the recommendation. 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. Bare except having different meaning ------------------------------------ With several of the proposed syntaxes, omitting the exception type name would be easy and concise, and would be tempting. For convenience's sake, it might be advantageous to have a bare 'except' clause mean something more useful than "except BaseException". Proposals included having it catch Exception, or some specific set of "common exceptions" (subclasses of a new type called ExpressionError), or have it look for a tuple named ExpressionError in the current scope, with a built-in default such as (ValueError, UnicodeError, AttributeError, EOFError, IOError, OSError, LookupError, NameError, ZeroDivisionError). All of these were rejected, for severa reasons. * First and foremost, consistency with the statement form of try/except would be broken. Just as a list comprehension or ternary if expression can be explained by "breaking it out" into its vertical statement form, an expression-except should be able to be explained by a relatively mechanical translation into a near-equivalent statement. Any form of syntax common to both should therefore have the same semantics in each, and above all should not have the subtle difference of catching more in one than the other, as it will tend to attract unnoticed bugs. * Secondly, the set of appropriate exceptions to catch would itself be a huge point of contention. It would be impossible to predict exactly which exceptions would "make sense" to be caught; why bless some of them with convenient syntax and not others? * And finally (this partly because the recommendation was that a bare except should be actively encouraged, once it was reduced to a "reasonable" set of exceptions), any situation where you catch an exception you don't expect to catch is an unnecessary bug magnet. Consequently, the use of a bare 'except' is down to two possibilities: either it is syntactically forbidden in the expression form, or it is permitted with the exact same semantics as in the statement form (namely, that it catch BaseException and be unable to capture it with 'as'). Bare except clauses ------------------- PEP 8 rightly advises against the use of a bare 'except'. While it is syntactically legal in a statement, and for backward compatibility must remain so, there is little value in encouraging its use. In an expression except clause, "except:" is a SyntaxError; use the equivalent long-hand form "except BaseException:" instead. A future version of Python MAY choose to reinstate this, which can be done without breaking compatibility. 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 ) This is more compelling when one or both of the deferred sub-proposals of multiple except clauses and/or exception capturing is included. In their absence, the parentheses would be thus:: value = expr except ExceptionType: default value = expr (except ExceptionType: default) The advantage is minimal, and the potential to confuse a reader into thinking the except clause is separate from the expression, or into thinking this is a function call, makes this non-compelling. The expression can, of course, be parenthesized if desired, as can the default:: value = (expr) except ExceptionType: (default) Short-hand for "except: pass" ----------------------------- The following was been suggested as a similar short-hand, though not technically an expression:: statement except Exception: pass try: statement except Exception: pass For instance, a common use-case is attempting the removal of a file:: os.unlink(some_file) except OSError: pass There is an equivalent already in Python 3.4, however, in contextlib:: from contextlib import suppress with suppress(OSError): os.unlink(some_file) As this is already a single line (or two with a break after the colon), there is little need of new syntax and a confusion of statement vs expression to achieve this. 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:
On 02/20/2014 07:15 PM, Chris Angelico wrote:
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, 21-Feb-2014
Nice work with the PEP, Chris! +1 to general idea. (I'll vote on the syntax when the bikeshedding begins ;). -- ~Ethan~
On 02/20/2014 09:00 PM, Chris Angelico wrote:
On Fri, Feb 21, 2014 at 3:54 PM, Ethan Furman <ethan@stoneleaf.us> wrote:
(I'll vote on the syntax when the bikeshedding begins ;).
Go get the keys to the time machine! Bikeshedding begins a week ago.
Oh, no, not on PyDev it hasn't! Get ready for round 2! -- ~Ethan~
On 21 February 2014 13:15, Chris Angelico <rosuav@gmail.com> wrote:
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, 21-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.
Great work on this Chris - this is one of the best researched and justified Python syntax proposals I've seen :)
Open Issues ===========
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
I'd like to make the case that the PEP should adopt this as its default position. My rationale is mainly that if we start by requiring the parentheses, it's pretty straightforward to take that requirement away in specific cases later, as well as making it easier to introduce multiple except clauses if that ever seems necessary. However, if we start without the required parentheses, that's it - we can't introduce a requirement for parentheses later if we decide the bare form is too confusing in too many contexts, and there's plenty of potential for such confusion. In addition to the odd interactions with other uses of the colon as a marker in the syntax, including suite headers, lambdas and function annotations, omitting the parentheses makes it harder to decide which behaviour was intended in ambiguous cases, while the explicit parentheses would force the writer to be clear which one they meant. Consider: x = get_value() except NotFound: foo is not None There are two plausible interpretations of that: x = (get_value() except NotFound: foo) is not None x = get_value() except NotFound: (foo is not None) With the proposed precedence in the PEP (which I agree with), the latter is the correct interpretation, but that's not at all obvious to the reader - they would have to "just know" that's the way it works. By contrast, if the parentheses are required, then the spelling would have to be one of the following to be legal: x = (get_value() except NotFound: foo is not None) x = (get_value() except NotFound: foo) is not None Which means the ":" and the closing ")" nicely bracket the fallback value in both cases and make the author's intent relatively clear. The required parentheses also help in the cases where there is a nearby colon with a different meaning: if check() except Exception: False: ... if (check() except Exception: False): ... lambda x: calculate(x) except Exception: None lambda x: (calculate(x) except Exception: None) def f(a: "OS dependent" = os_defaults[os.name] except KeyError: None): pass def f(a: "OS dependent" = (os_defaults[os.name] except KeyError: None)): pass Rather than making people consider "do I need the parentheses in this case or not?", adopting the genexp rule makes it simple: yes, you need them, because the compiler will complain if you leave them out.
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")
I don't think taking it all the way to one expression shows the new construct in the best light. Keeping this as multiple statements assigning to a temporary variable improves the readability quite a bit: if not check_permission(k): msg = 'Access denied' else: msg = (cache[k] except LookupError: None) if msg is None: msg = (backend.read(k) except OSError: 'Resource not available') logging.info("Message shown to user: %s", msg) I would also move the "bare except clause" equivalent out to a separate example. Remember, you're trying to convince people to *like* the PEP, not scare them away with the consequences of what happens when people try to jam too much application logic into a single statement. While we're admittedly giving people another tool to help them win obfuscated Python contests, we don't have to *encourage* them :)
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", _)
A real variable name like "msg" would also be appropriate in the expanded form of this particular example.
Deferred sub-proposals ======================
Capturing the exception object ------------------------------
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), it is extremely *uncommon* in the cases which could logically be converted into the expression form. Its few uses can simply be left unchanged. Consequently, in the interests of simplicity, the 'as' clause is not included in this proposal. A subsequent Python version can add this without breaking any existing code, as 'as' is already a keyword.
We can't defer this one - if we don't implement it now, we should reject it as a future addition. The reason we can't defer it is subtle, but relatively easy to demonstrate with a list comprehension: >>> i = 1 >>> [i for __ in range(1)] [1] >>> class C: ... j = 2 ... [i for __ in range(1)] ... [j for __ in range(1)] ... Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 4, in C File "<stdin>", line 4, in <listcomp> NameError: global name 'j' is not defined The problem is that "<listcomp>" scope that is visible in the traceback - because it's a true closure, it's treated the same as any other function defined inside a class body, and the subexpressions can't see the class level variables. An except expression that allowed capturing of the exception would need to behave the same way in order to be consistent, but if we don't allow capturing, then we can dispense with the closure entirely. However, if we do that, we can't decide to add capturing later, because that would mean adding the closure, which would be potentially backwards incompatible with usage at class scopes. And if we only add the closure if the exception is captured, then the behaviour of the other subexpressions will depend on whether the exception is captured or not, and that's just messy. So I think it makes more sense to reject this subproposal outright - it makes the change more complex and make the handling of the common case worse, for the sake of something that will almost never be an appropriate thing to do. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On 21 February 2014 11:35, Nick Coghlan <ncoghlan@gmail.com> wrote:
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.
Great work on this Chris - this is one of the best researched and justified Python syntax proposals I've seen :)
Agreed - given the number of differing opinions on python-ideas, it's particularly impressive how well the debate was conducted too. Paul
On Fri, Feb 21, 2014 at 10:35 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
On 21 February 2014 13:15, Chris Angelico <rosuav@gmail.com> wrote:
PEP: 463 Title: Exception-catching expressions Great work on this Chris - this is one of the best researched and justified Python syntax proposals I've seen :)
It is? Wow... I'm not sure what that says about other syntax proposals. This is one week's python-ideas discussion plus one little script doing analysis on the standard library. Hardly PhD level research :)
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
I'd like to make the case that the PEP should adopt this as its default position. My rationale is mainly that if we start by requiring the parentheses, it's pretty straightforward to take that requirement away in specific cases later, as well as making it easier to introduce multiple except clauses if that ever seems necessary.
However, if we start without the required parentheses, that's it - we can't introduce a requirement for parentheses later if we decide the bare form is too confusing in too many contexts, and there's plenty of potential for such confusion.
If once the parens are made mandatory, they'll most likely stay mandatory forever - I can't imagine there being any strong impetus to change the language to make them optional.
The required parentheses also help in the cases where there is a nearby colon with a different meaning:
if check() except Exception: False: ... if (check() except Exception: False):
People can already write: if (x if y else z): without the parens, and it works. Readability suffers when the same keyword is used twice (here "if" rather than the colon, but same difference), yet the parens are considered optional. Python is a language that, by and large, lacks syntactic salt; style guides are free to stipulate more, but the language doesn't make demands. I would strongly *recommend* using parens in all the cases you've shown, especially lambda:
lambda x: calculate(x) except Exception: None lambda x: (calculate(x) except Exception: None)
as it would otherwise depend on operator precedence; but mandating them feels to me like demanding readability.
def f(a: "OS dependent" = os_defaults[os.name] except KeyError: None): pass def f(a: "OS dependent" = (os_defaults[os.name] except KeyError: None)): pass
Ehh, that one's a mess. I'd be looking at breaking out the default: default = os_defaults[os.name] except KeyError: None def f(a: "OS dependent" = default): pass with possibly some better name than 'default'. The one-liner is almost 80 characters long without indentation and with very short names. But if it's okay to wrap it, that would work without the parens: def f( a: "OS dependent" = os_defaults[os.name] except KeyError: None, another_arg ........., more, args ......, ): pass Clarity is maintained by judicious placement of newlines just as much as by parentheses.
Rather than making people consider "do I need the parentheses in this case or not?", adopting the genexp rule makes it simple: yes, you need them, because the compiler will complain if you leave them out.
Yes, that is a reasonable line of argument. On the other hand, there's no demand for parens when you mix and and or: x or y and z I'd wager more than half of Python programmers would be unable to say for sure which would be evaluated first. The compiler could have been written to reject this (by placing and and or at the same precedence and having no associativity - I'm not sure if the current lexer in CPython can do that, but it's certainly not conceptually inconceivable), but a decision was made to make this legal.
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")
I don't think taking it all the way to one expression shows the new construct in the best light. Keeping this as multiple statements assigning to a temporary variable improves the readability quite a bit:
Yeah, good point. I tried to strike a balance between simple and complex examples, but it's hard to judge.
I would also move the "bare except clause" equivalent out to a separate example. Remember, you're trying to convince people to *like* the PEP, not scare them away with the consequences of what happens when people try to jam too much application logic into a single statement. While we're admittedly giving people another tool to help them win obfuscated Python contests, we don't have to *encourage* them :)
Heh. That example was written when an actual bare except clause was part of the proposal. I'll drop that entire example; it's contrived, and we have a number of concrete examples now.
Capturing the exception object ------------------------------
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), it is extremely *uncommon* in the cases which could logically be converted into the expression form. Its few uses can simply be left unchanged. Consequently, in the interests of simplicity, the 'as' clause is not included in this proposal. A subsequent Python version can add this without breaking any existing code, as 'as' is already a keyword.
We can't defer this one - if we don't implement it now, we should reject it as a future addition. The reason we can't defer it is [ chomped for brevity ]
So I think it makes more sense to reject this subproposal outright - it makes the change more complex and make the handling of the common case worse, for the sake of something that will almost never be an appropriate thing to do.
Interesting. That's an aspect I never thought of. Suppose Python were to add support for anonymous sub-scopes. This could be applied to every current use of 'as', as well as comprehensions: spam = "Original spam" try: 1/0 except Exception as spam: assert isinstance(spam, Exception) with open(fn) as spam: assert hasattr(spam, "read") [spam for spam in [1,2,3]] assert spam == "Original spam" It'd be a backward-incompatible change, but it's more likely to be what people expect. The general assumption of "with ... as ..." is that the thing should be used inside the block, and should be finished with when you exit the block, so having the name valid only inside the block does make sense. That's a completely separate proposal. But suppose that were to happen, and to not require a closure. It would then make good sense to be able to capture an exception inside an expression - and it could be done without breaking anything. So, if it is to be rejected, I'd say it's on the technical grounds that it would require a closure in CPython, and that a closure is incompatible with the current proposal. Does that sound right? (It's also not a huge loss, since it's almost unused. But it's an incompatibility between statement and expression form.) ChrisA
On Fri, Feb 21, 2014 at 11:37 PM, Paul Moore <p.f.moore@gmail.com> wrote:
On 21 February 2014 11:35, Nick Coghlan <ncoghlan@gmail.com> wrote:
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.
Great work on this Chris - this is one of the best researched and justified Python syntax proposals I've seen :)
Agreed - given the number of differing opinions on python-ideas, it's particularly impressive how well the debate was conducted too.
On that subject, I'd like to thank everyone involved in the python-ideas discussion, and particularly Steven D'Aprano, Rob Cliffe, and Andrew Barnert; it was an amazingly productive thread, getting to nearly four hundred emails before seriously meandering. And even then, it mostly just started looping back on itself, which isn't surprising given that it was so long - anyone not majorly invested in the topic won't have read every single post. ChrisA
On 21 February 2014 22:42, Chris Angelico <rosuav@gmail.com> wrote:
It'd be a backward-incompatible change, but it's more likely to be what people expect. The general assumption of "with ... as ..." is that the thing should be used inside the block, and should be finished with when you exit the block, so having the name valid only inside the block does make sense.
That's a completely separate proposal. But suppose that were to happen, and to not require a closure. It would then make good sense to be able to capture an exception inside an expression - and it could be done without breaking anything.
So, if it is to be rejected, I'd say it's on the technical grounds that it would require a closure in CPython, and that a closure is incompatible with the current proposal. Does that sound right?
(It's also not a huge loss, since it's almost unused. But it's an incompatibility between statement and expression form.)
It's probably OK to leave it in the deferred section and just note the difficulty of implementing it in a backwards compatible way, since we're *not* going to be introducing a closure. Python 3 except clauses are already a bit weird anyway, since they do an implicit delete at the end, but only if the except clause is actually triggered:
e = "Hello" try: ... 1/0 ... except ZeroDivisionError as e: ... pass ... e Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'e' is not defined e = "Hello" try: ... pass ... except Exception as e: ... pass ... e 'Hello'
Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On 21 February 2014 22:42, Chris Angelico <rosuav@gmail.com> wrote:
On Fri, Feb 21, 2014 at 10:35 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
On 21 February 2014 13:15, Chris Angelico <rosuav@gmail.com> wrote:
PEP: 463 Title: Exception-catching expressions Great work on this Chris - this is one of the best researched and justified Python syntax proposals I've seen :)
It is? Wow... I'm not sure what that says about other syntax proposals. This is one week's python-ideas discussion plus one little script doing analysis on the standard library. Hardly PhD level research :)
Right, it just takes someone willing to put in the time to actually put a concrete proposal together, read all the discussions, attempt to summarise them into a coherent form and go looking for specific examples to help make their case. I think a large part of my pleased reaction is the fact that several of the python-ideas regulars (including me) have a habit of responding to new syntax proposals with a list of things to do to make a good PEP (especially Raymond's "search the stdlib for code that would be improved" criterion), and it's quite a novelty to have someone take that advice and put together a compelling argument - the more typical reaction is for the poster to decide that a PEP sounds like too much work and drop the idea (or else to realise that they can't actually provide the compelling use cases requested). As you have discovered, creating a PEP really isn't that arduous, so long as you have enough spare time to keep up with the discussions, and there actually are compelling examples of possible improvements readily available :) Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Fri, 21 Feb 2014 14:15:59 +1100 Chris Angelico <rosuav@gmail.com> wrote:
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.
While I sympathize with the motivation, I really don't like the end result:
lst = [1, 2] value = lst[2] except IndexError: "No value"
is too much of a break from the usual stylistic conventions, and looks like several statements were stuffed on a single line. In other words, the gain in concision is counterbalanced by a loss in readability, a compromise which doesn't fit in Python's overall design principles. (compare with "with", which solves actual readability issues due to the distance between the "try" and the "finally" clause, and also promotes better resource management) So -0.5 from me. Regards Antoine.
On 21 February 2014 22:42, Chris Angelico <rosuav@gmail.com> wrote:
People can already write:
if (x if y else z):
without the parens, and it works. Readability suffers when the same keyword is used twice (here "if" rather than the colon, but same difference), yet the parens are considered optional. Python is a language that, by and large, lacks syntactic salt; style guides are free to stipulate more, but the language doesn't make demands. I would strongly *recommend* using parens in all the cases you've shown, especially lambda:
lambda x: calculate(x) except Exception: None lambda x: (calculate(x) except Exception: None)
as it would otherwise depend on operator precedence; but mandating them feels to me like demanding readability.
Right, that's why my main motivation for this suggestion is the one relating to keeping future options open. If the parentheses are optional, than adding multiple except clauses latter isn't possible, since this would already be valid, but mean something different: expr except Exception1: default1 except Exception2: default2 The deferral currently has this snippet: """In order to ensure compatibility with future versions, ensure that any consecutive except operators are parenthesized to guarantee the interpretation you expect.""" That's not a reasonable expectation - either the parentheses have to be mandatory as part of the deferral, or else multiple except clause support needs to be listed as rejected rather than deferred. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Thu, Feb 20, 2014 at 7:15 PM, Chris Angelico <rosuav@gmail.com> wrote:
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, 21-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.
Chris, while I also commend you for the comprehensive PEP, I'm -1 on the proposal, for two main reasons: 1. Many proposals suggest new syntax to gain some succinctness. Each has to be judged for its own merits, and in this case IMHO the cons eclipse the pros. I don't think this will save a lot of code in a typical well-structured program - maybe a few lines out of hundreds. On the other hand, it adds yet another syntax to remember and understand, which is not the Pythonic way. 2. Worse, this idea subverts exceptions to control flow, which is not only un-Pythonic but also against the accepted practices of programming in general. Here, the comparison to PEP 308 is misguided. PEP 308, whatever syntax it adds, still remains within the domain of normal control flow. PEP 463, OTOH, makes it deliberately easy to make exceptions part of non-exceptional code, encouraging very bad programming practices. Eli
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
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.
With some situations, an LBYL technique can be used (checking if some sequence has enough length before indexing into it, for instance). This is not safe in all cases, but as it is often convenient, programmers will be tempted to sacrifice the safety of EAFP in favour of the notational brevity of LBYL. Additionally, some LBYL techniques (eg involving getattr with three arguments) warp the code into looking like literal strings rather than attribute lookup, which can impact readability. A convenient EAFP notation solves all of this.
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: return default() value = except_(lambda: 1/x, ZeroDivisionError, lambda: 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 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 of the overall expression. If any exception is raised, exception_list 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, non-matching exceptions will propagate upward.
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.)
This ternary operator would be between lambda and if/else in precedence.
Consider this example of a two-level cache:: for key in sequence: x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key))) # do something with x
This cannot be rewritten as:: x = lvl1.get(key, lvl2.get(key, f(key)))
which, despite being shorter, defeats the purpose of the cache, as it must calculate a default value to pass to get(). The .get() version calculates backwards; the exception-testing version calculates forwards, as would be expected. The nearest useful equivalent would be:: x = lvl1.get(key) or lvl2.get(key) or f(key) which depends on the values being nonzero, as well as depending on the cache object supporting this functionality.
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 default # Catches anything value = expr except(Exception) default # Catches only the named type(s) value = default if expr raise Exception value = expr or else default if Exception value = 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'. Also, in the context of the "default if expr raise Exception" proposal, it was suggested that a new keyword "raises" be used.
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.
Open Issues ===========
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
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.
A number of these examples are taken directly from the Python standard library, with file names and line numbers correct as of early Feb 2014. Many of these patterns are extremely common.
Retrieve an argument, defaulting to None:: cond = args[1] except IndexError: None
# Lib/pdb.py:803: try: cond = args[1] except IndexError: cond = None
Fetch information from the system if available:: pwd = os.getcwd() except OSError: None
# Lib/tkinter/filedialog.py:210: try: pwd = os.getcwd() except OSError: pwd = None
Attempt a translation, falling back on the original:: e.widget = self._nametowidget(W) except KeyError: W
# Lib/tkinter/__init__.py:1222: try: e.widget = self._nametowidget(W) except KeyError: e.widget = W
Read from an iterator, continuing with blank lines once it's exhausted:: line = readline() except StopIteration: ''
# Lib/lib2to3/pgen2/tokenize.py:370: try: line = readline() except StopIteration: line = ''
Retrieve platform-specific information (note the DRY improvement); 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: ''
# Lib/sysconfig.py:529: try: _CONFIG_VARS['abiflags'] = sys.abiflags except AttributeError: # sys.abiflags may not be defined on all platforms. _CONFIG_VARS['abiflags'] = ''
Retrieve an indexed item, defaulting to None (similar to dict.get):: def getNamedItem(self, name): return self._attrs[name] except KeyError: None
# Lib/xml/dom/minidom.py:573: def getNamedItem(self, name): try: return self._attrs[name] except KeyError: return None
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
# Lib/tarfile.py:2198: try: g = grp.getgrnam(tarinfo.gname)[2] except KeyError: g = tarinfo.gid try: u = pwd.getpwnam(tarinfo.uname)[2] except KeyError: u = tarinfo.uid
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")
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
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()
try: (overrides[x] or default).ping() except IndexError: default.ping()
Narrowing of exception-catching scope -------------------------------------
The following examples, taken directly from Python's standard library, demonstrate how the scope of the try/except can be conveniently narrowed. To do this with the statement form of try/except would require a temporary variable, but it's far cleaner as an expression.
Lib/ipaddress.py:343:: try: ips.append(ip.ip) except AttributeError: ips.append(ip.network_address) Becomes:: ips.append(ip.ip except AttributeError: ip.network_address) The expression form is nearly equivalent to this:: try: _ = ip.ip except AttributeError: _ = ip.network_address ips.append(_)
Lib/tempfile.py:130:: try: dirlist.append(_os.getcwd()) except (AttributeError, OSError): dirlist.append(_os.curdir) Becomes:: dirlist.append(_os.getcwd() except (AttributeError, OSError): _os.curdir)
Lib/asyncore.py:264:: try: status.append('%s:%d' % self.addr) except TypeError: status.append(repr(self.addr)) Becomes:: status.append('%s:%d' % self.addr except TypeError: repr(self.addr))
Comparisons with other languages ================================
(With thanks to Andrew Barnert for compiling this section.)
`Ruby's`__ "begin...rescue...rescue...else...ensure...end" is an expression (potentially with statements inside it). It has the equivalent of an "as" clause, and the equivalent of bare except. And it uses no punctuation or keyword between the bare except/exception class/exception class with as clause and the value. (And yes, it's ambiguous unless you understand Ruby's statement/expression rules.)
__ http://www.skorks.com/2009/09/ruby-exceptions-and-exception-handling/
::
x = begin computation() rescue MyException => e default(e) end; x = begin computation() rescue MyException default() end; x = begin computation() rescue default() end; x = begin computation() rescue MyException default() rescue OtherException other() end;
In terms of this PEP::
x = computation() except MyException as e default(e) x = computation() except MyException default(e) x = computation() except default(e) x = computation() except MyException default() except OtherException other()
`Erlang`__ has a try expression that looks like this::
__ http://erlang.org/doc/reference_manual/expressions.html#id79284
x = try computation() catch MyException:e -> default(e) end; x = try computation() catch MyException:e -> default(e); OtherException:e -> other(e) end;
The class and "as" name are mandatory, but you can use "_" for either. There's also an optional "when" guard on each, and a "throw" clause that you can catch, which I won't get into. To handle multiple exceptions, you just separate the clauses with semicolons, which I guess would map to commas in Python. So::
x = try computation() except MyException as e -> default(e) x = try computation() except MyException as e -> default(e), OtherException as e->other_default(e)
Erlang also has a "catch" expression, which, despite using the same keyword, is completely different, and you don't want to know about it.
The ML family has two different ways of dealing with this, "handle" and "try"; the difference between the two is that "try" pattern-matches the exception, which gives you the effect of multiple except clauses and as clauses. In either form, the handler clause is punctuated by "=>" in some dialects, "->" in others.
To avoid confusion, I'll write the function calls in Python style.
Here's `SML's`__ "handle":: __ http://www.cs.cmu.edu/~rwh/introsml/core/exceptions.htm
let x = computation() handle MyException => default();;
Here's `OCaml's`__ "try":: __ http://www2.lib.uchicago.edu/keith/ocaml-class/exceptions.html
let x = try computation() with MyException explanation -> default(explanation);;
let x = try computation() with
MyException(e) -> default(e) | MyOtherException() -> other_default() | (e) -> fallback(e);;
In terms of this PEP, these would be something like::
x = computation() except MyException => default() x = try computation() except MyException e -> default() x = (try computation() except MyException as e -> default(e) except MyOtherException -> other_default() except BaseException as e -> fallback(e))
Many ML-inspired but not-directly-related languages from academia mix things up, usually using more keywords and fewer symbols. So, the `Oz`__ would map to Python as:: __ http://mozart.github.io/mozart-v1/doc-1.4.0/tutorial/node5.html
x = try computation() catch MyException as e then default(e)
Many Lisp-derived languages, like `Clojure,`__ implement try/catch as special forms (if you don't know what that means, think function-like macros), so you write, effectively:: __ http://clojure.org/special_forms#Special%20Forms--(try%20expr*%20catch-claus... ?)
try(computation(), catch(MyException, explanation, default(explanation)))
try(computation(), catch(MyException, explanation, default(explanation)), catch(MyOtherException, explanation, other_default(explanation)))
In Common Lisp, this is done with a slightly clunkier `"handler-case" macro,`__ but the basic idea is the same.
__ http://clhs.lisp.se/Body/m_hand_1.htm
The Lisp style is, surprisingly, used by some languages that don't have macros, like Lua, where `xpcall`__ takes functions. Writing lambdas Python-style instead of Lua-style:: __ http://www.gammon.com.au/scripts/doc.php?lua=xpcall
x = xpcall(lambda: expression(), lambda e: default(e))
This actually returns (true, expression()) or (false, default(e)), but I think we can ignore that part.
Haskell is actually similar to Lua here (except that it's all done with monads, of course)::
x = do catch(lambda: expression(), lambda e: default(e))
You can write a pattern matching expression within the function to decide what to do with it; catching and re-raising exceptions you don't want is cheap enough to be idiomatic.
But Haskell infixing makes this nicer::
x = do expression() `catch` lambda: default() x = do expression() `catch` lambda e: default(e)
And that makes the parallel between the lambda colon and the except colon in the proposal much more obvious::
x = expression() except Exception: default() x = expression() except Exception as e: default(e)
`Tcl`__ has the other half of Lua's xpcall; catch is a function which returns true if an exception was caught, false otherwise, and you get the value out in other ways. And it's all built around the the implicit quote-and-exec that everything in Tcl is based on, making it even harder to describe in Python terms than Lisp macros, but something like:: __ http://wiki.tcl.tk/902
if {[ catch("computation()") "explanation"]} { default(explanation) }
`Smalltalk`__ is also somewhat hard to map to Python. The basic version would be:: __ http://smalltalk.gnu.org/wiki/exceptions
x := computation() on:MyException do:default()
... but that's basically Smalltalk's passing-arguments-with-colons syntax, not its exception-handling syntax.
Deferred sub-proposals ======================
Multiple except clauses -----------------------
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 )
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.
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.
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.)
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
(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
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), it is extremely *uncommon* in the cases which could logically be converted into the expression form. Its few uses can simply be left unchanged. Consequently, in the interests of simplicity, the 'as' clause is not included in this proposal. A subsequent Python version can add this without breaking any existing code, as 'as' is already a keyword.
One example where this could possibly be useful is Lib/imaplib.py:568:: try: typ, dat = self._simple_command('LOGOUT') except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]] This could become:: typ, dat = (self._simple_command('LOGOUT') except BaseException as e: ('NO', '%s: %s' % (type(e), e))) Or perhaps some other variation. This is hardly the most compelling use-case, but an intelligent look at this code could tidy it up significantly. In the absence of further examples showing any need of the exception object, I have opted to defer indefinitely the recommendation.
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.
Bare except having different meaning ------------------------------------
With several of the proposed syntaxes, omitting the exception type name would be easy and concise, and would be tempting. For convenience's sake, it might be advantageous to have a bare 'except' clause mean something more useful than "except BaseException". Proposals included having it catch Exception, or some specific set of "common exceptions" (subclasses of a new type called ExpressionError), or have it look for a tuple named ExpressionError in the current scope, with a built-in default such as (ValueError, UnicodeError, AttributeError, EOFError, IOError, OSError, LookupError, NameError, ZeroDivisionError). All of these were rejected, for severa reasons.
* First and foremost, consistency with the statement form of try/except would be broken. Just as a list comprehension or ternary if expression can be explained by "breaking it out" into its vertical statement form, an expression-except should be able to be explained by a relatively mechanical translation into a near-equivalent statement. Any form of syntax common to both should therefore have the same semantics in each, and above all should not have the subtle difference of catching more in one than the other, as it will tend to attract unnoticed bugs.
* Secondly, the set of appropriate exceptions to catch would itself be a huge point of contention. It would be impossible to predict exactly which exceptions would "make sense" to be caught; why bless some of them with convenient syntax and not others?
* And finally (this partly because the recommendation was that a bare except should be actively encouraged, once it was reduced to a "reasonable" set of exceptions), any situation where you catch an exception you don't expect to catch is an unnecessary bug magnet.
Consequently, the use of a bare 'except' is down to two possibilities: either it is syntactically forbidden in the expression form, or it is permitted with the exact same semantics as in the statement form (namely, that it catch BaseException and be unable to capture it with 'as').
Bare except clauses -------------------
PEP 8 rightly advises against the use of a bare 'except'. While it is syntactically legal in a statement, and for backward compatibility must remain so, there is little value in encouraging its use. In an expression except clause, "except:" is a SyntaxError; use the equivalent long-hand form "except BaseException:" instead. A future version of Python MAY choose to reinstate this, which can be done without breaking compatibility.
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 )
This is more compelling when one or both of the deferred sub-proposals of multiple except clauses and/or exception capturing is included. In their absence, the parentheses would be thus:: value = expr except ExceptionType: default value = expr (except ExceptionType: default)
The advantage is minimal, and the potential to confuse a reader into thinking the except clause is separate from the expression, or into thinking this is a function call, makes this non-compelling. The expression can, of course, be parenthesized if desired, as can the default:: value = (expr) except ExceptionType: (default)
Short-hand for "except: pass" -----------------------------
The following was been suggested as a similar short-hand, though not technically an expression::
statement except Exception: pass
try: statement except Exception: pass
For instance, a common use-case is attempting the removal of a file:: os.unlink(some_file) except OSError: pass
There is an equivalent already in Python 3.4, however, in contextlib:: from contextlib import suppress with suppress(OSError): os.unlink(some_file)
As this is already a single line (or two with a break after the colon), there is little need of new syntax and a confusion of statement vs expression to achieve this.
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: _______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/eliben%40gmail.com
My 2 cents worth: On 21/02/2014 12:42, Chris Angelico wrote:
On Fri, Feb 21, 2014 at 10:35 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
Great work on this Chris - this is one of the best researched and justified Python syntax proposals I've seen :) Hear, hear! ("Praise from the praiseworthy is praise indeed" - Tolkien.)
Small point: in one of your examples you give a plug for the PEP "note the DRY improvement". I would suggest that similarly perhaps in your Lib/tarfile.py:2198 example you point out the increase in readability due to the 2 lines lining up in your Lib/ipaddress.py:343 example you point out that the new form is probably an improvement (i.e. probably closer to the author's intention) as it will NOT catch an AttributeError evaluating "ips.append" (this would matter e.g. if "append" were mis-spelt). YOU are clearly aware of this but it would often escape the casual reader.
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") I don't think taking it all the way to one expression shows the new construct in the best light. Keeping this as multiple statements assigning to a temporary variable improves the readability quite a bit: I agree, it looks scary (too many branches in one statement). The shorter the example, the more convincing. Yeah, good point. I tried to strike a balance between simple and complex examples, but it's hard to judge.
Capturing the exception object ------------------------------
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), it is extremely *uncommon* in the cases which could logically be converted into the expression form. Its few uses can simply be left unchanged. Consequently, in the interests of simplicity, the 'as' clause is not included in this proposal. A subsequent Python version can add this without breaking any existing code, as 'as' is already a keyword. We can't defer this one - if we don't implement it now, we should reject it as a future addition. The reason we can't defer it is [ chomped for brevity ]
So I think it makes more sense to reject this subproposal outright - it makes the change more complex and make the handling of the common case worse, for the sake of something that will almost never be an appropriate thing to do. Thanks for looking into this Nick. I confess I don't entirely understand the technical argument (my understanding breaks down at "the subexpressions can't see the class level variables", but I don't want to waste anybody's time expecting an explanation, I can always look into it myself) but I accept that there is a good reason for disallowing 'as' and its usage would be rare, so I will (slightly regretfully) wave it goodbye.
So, if it is to be rejected, I'd say it's on the technical grounds that it would require a closure in CPython, and that a closure is incompatible with the current proposal. Does that sound right?
It does to me FWIW.
(It's also not a huge loss, since it's almost unused. But it's an incompatibility between statement and expression form.)
ChrisA
Assuming the PEP is accepted (not a trivial assumption!) I hope that at some stage allowing multiple except clauses (to trap different exceptions raised by the original expression) will be reinstated. But I have to accept that it would be rarely used. To my mind the real unsung hero of this story is the existing "try ... except ... finally" syntax and its flexibility. In my code base there were a few hundred of these, but just over a dozen where the new syntax would be appropriate. (Hope I'm not starting to argue against the PEP!) Rob Cliffe
_______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/rob.cliffe%40btinternet.c...
----- No virus found in this message. Checked by AVG - www.avg.com Version: 2012.0.2247 / Virus Database: 3705/6611 - Release Date: 02/20/14
On 22 February 2014 00:03, Eli Bendersky <eliben@gmail.com> wrote:
On Thu, Feb 20, 2014 at 7:15 PM, Chris Angelico <rosuav@gmail.com> wrote:
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, 21-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.
Chris, while I also commend you for the comprehensive PEP, I'm -1 on the proposal, for two main reasons:
1. Many proposals suggest new syntax to gain some succinctness. Each has to be judged for its own merits, and in this case IMHO the cons eclipse the pros. I don't think this will save a lot of code in a typical well-structured program - maybe a few lines out of hundreds. On the other hand, it adds yet another syntax to remember and understand, which is not the Pythonic way.
2. Worse, this idea subverts exceptions to control flow, which is not only un-Pythonic but also against the accepted practices of programming in general. Here, the comparison to PEP 308 is misguided. PEP 308, whatever syntax it adds, still remains within the domain of normal control flow. PEP 463, OTOH, makes it deliberately easy to make exceptions part of non-exceptional code, encouraging very bad programming practices.
Neither of these objections addresses the problems with the status quo, though: - the status quo encourages overbroad exception handling (as illustrated by examples in the PEP) - the status quo encourages an ad hoc approach to hiding exception handling inside functions PEP 308 was accepted primarily because the and/or hack was a bug magnet. The status quo for exception handling is both a bug magnet (due to overbroad exception handlers), and a cause of creeping complexity in API design (as more and more APIs, both in the standard library and elsewhere, acquire ways to request implicit exception handling). That's why the comparison to PEP 308 is appropriate: it's less about making the language better directly, and more about deciding the consequences of not having it are no longer acceptable. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Fri, Feb 21, 2014 at 8:52 AM, Antoine Pitrou <solipsis@pitrou.net> wrote:
On Fri, 21 Feb 2014 14:15:59 +1100 Chris Angelico <rosuav@gmail.com> wrote:
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.
While I sympathize with the motivation, I really don't like the end result:
lst = [1, 2] value = lst[2] except IndexError: "No value"
is too much of a break from the usual stylistic conventions, and looks like several statements were stuffed on a single line.
In other words, the gain in concision is counterbalanced by a loss in readability, a compromise which doesn't fit in Python's overall design principles.
(compare with "with", which solves actual readability issues due to the distance between the "try" and the "finally" clause, and also promotes better resource management)
While I like the general concept, I agree that it looks too much like a crunched statement; the use of the colon is a non-starter for me. I'm sure I'm not the only one whose brain has been trained to view a colon in Python to mean "statement", period. This goes against that syntactic practice and just doesn't work for me. I'm -1 with the current syntax, but it can go into the + range if a better syntax can be chosen.
On Fri, Feb 21, 2014 at 6:28 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
On 22 February 2014 00:03, Eli Bendersky <eliben@gmail.com> wrote:
On Thu, Feb 20, 2014 at 7:15 PM, Chris Angelico <rosuav@gmail.com> wrote:
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, 21-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.
Chris, while I also commend you for the comprehensive PEP, I'm -1 on the proposal, for two main reasons:
1. Many proposals suggest new syntax to gain some succinctness. Each has to be judged for its own merits, and in this case IMHO the cons eclipse the pros. I don't think this will save a lot of code in a typical well-structured program - maybe a few lines out of hundreds. On the other hand, it adds yet another syntax to remember and understand, which is not the Pythonic way.
2. Worse, this idea subverts exceptions to control flow, which is not only un-Pythonic but also against the accepted practices of programming in general. Here, the comparison to PEP 308 is misguided. PEP 308, whatever syntax it adds, still remains within the domain of normal control flow. PEP 463, OTOH, makes it deliberately easy to make exceptions part of non-exceptional code, encouraging very bad programming practices.
Neither of these objections addresses the problems with the status quo, though:
- the status quo encourages overbroad exception handling (as illustrated by examples in the PEP) - the status quo encourages an ad hoc approach to hiding exception handling inside functions
I think the PEP, and your reply, focuses too much on one single "status quo" situation, which is the dict.get-like usage. However, the PEP does not propose a narrow solution - it proposes a significant change in the way expressions may be written and exceptions may be caught, and thus opens a can of worms. Even if the status quo will be made better by it, and even this I'm not sure about (*), many many other possibilities of bad code open up. (*) IMHO it's better to hide these exceptions inside well defined functions -- because dict.get tells me it's the normal code flow, not the exceptional code flow. On the other hand, the syntax proposed herein tells me - yeah it's the exceptional code flow, but let me merge it into the normal code flow for you. This goes against anything I understand about how exceptions should and should not be used. Eli
PEP 308 was accepted primarily because the and/or hack was a bug magnet. The status quo for exception handling is both a bug magnet (due to overbroad exception handlers), and a cause of creeping complexity in API design (as more and more APIs, both in the standard library and elsewhere, acquire ways to request implicit exception handling).
That's why the comparison to PEP 308 is appropriate: it's less about making the language better directly, and more about deciding the consequences of not having it are no longer acceptable.
On Fri, Feb 21, 2014 at 9:03 AM, Eli Bendersky <eliben@gmail.com> wrote:
On Thu, Feb 20, 2014 at 7:15 PM, Chris Angelico <rosuav@gmail.com> wrote:
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, 21-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.
Chris, while I also commend you for the comprehensive PEP, I'm -1 on the proposal, for two main reasons:
1. Many proposals suggest new syntax to gain some succinctness. Each has to be judged for its own merits, and in this case IMHO the cons eclipse the pros. I don't think this will save a lot of code in a typical well-structured program - maybe a few lines out of hundreds. On the other hand, it adds yet another syntax to remember and understand, which is not the Pythonic way.
2. Worse, this idea subverts exceptions to control flow, which is not only un-Pythonic but also against the accepted practices of programming in general. Here, the comparison to PEP 308 is misguided. PEP 308, whatever syntax it adds, still remains within the domain of normal control flow. PEP 463, OTOH, makes it deliberately easy to make exceptions part of non-exceptional code, encouraging very bad programming practices.
But we subsumed using exception for control flow long ago. StopIteration is the most blatant, but when we added dict.get(), getattr() with a default value, iter() with its default, etc. we made it very clear that while exceptions typically represent exceptional situations, they can also be used as an easy signal of something which we expect you to handle and easily recover from (and returning None or some other singleton doesn't make sense), and yet without explicit API support it takes a heavy-handed statement to deal with. -Brett
Eli
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
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.
With some situations, an LBYL technique can be used (checking if some sequence has enough length before indexing into it, for instance). This is not safe in all cases, but as it is often convenient, programmers will be tempted to sacrifice the safety of EAFP in favour of the notational brevity of LBYL. Additionally, some LBYL techniques (eg involving getattr with three arguments) warp the code into looking like literal strings rather than attribute lookup, which can impact readability. A convenient EAFP notation solves all of this.
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: return default() value = except_(lambda: 1/x, ZeroDivisionError, lambda: 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 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 of the overall expression. If any exception is raised, exception_list 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, non-matching exceptions will propagate upward.
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.)
This ternary operator would be between lambda and if/else in precedence.
Consider this example of a two-level cache:: for key in sequence: x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key))) # do something with x
This cannot be rewritten as:: x = lvl1.get(key, lvl2.get(key, f(key)))
which, despite being shorter, defeats the purpose of the cache, as it must calculate a default value to pass to get(). The .get() version calculates backwards; the exception-testing version calculates forwards, as would be expected. The nearest useful equivalent would be:: x = lvl1.get(key) or lvl2.get(key) or f(key) which depends on the values being nonzero, as well as depending on the cache object supporting this functionality.
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 default # Catches anything value = expr except(Exception) default # Catches only the named type(s) value = default if expr raise Exception value = expr or else default if Exception value = 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'. Also, in the context of the "default if expr raise Exception" proposal, it was suggested that a new keyword "raises" be used.
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.
Open Issues ===========
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
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.
A number of these examples are taken directly from the Python standard library, with file names and line numbers correct as of early Feb 2014. Many of these patterns are extremely common.
Retrieve an argument, defaulting to None:: cond = args[1] except IndexError: None
# Lib/pdb.py:803: try: cond = args[1] except IndexError: cond = None
Fetch information from the system if available:: pwd = os.getcwd() except OSError: None
# Lib/tkinter/filedialog.py:210: try: pwd = os.getcwd() except OSError: pwd = None
Attempt a translation, falling back on the original:: e.widget = self._nametowidget(W) except KeyError: W
# Lib/tkinter/__init__.py:1222: try: e.widget = self._nametowidget(W) except KeyError: e.widget = W
Read from an iterator, continuing with blank lines once it's exhausted:: line = readline() except StopIteration: ''
# Lib/lib2to3/pgen2/tokenize.py:370: try: line = readline() except StopIteration: line = ''
Retrieve platform-specific information (note the DRY improvement); 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: ''
# Lib/sysconfig.py:529: try: _CONFIG_VARS['abiflags'] = sys.abiflags except AttributeError: # sys.abiflags may not be defined on all platforms. _CONFIG_VARS['abiflags'] = ''
Retrieve an indexed item, defaulting to None (similar to dict.get):: def getNamedItem(self, name): return self._attrs[name] except KeyError: None
# Lib/xml/dom/minidom.py:573: def getNamedItem(self, name): try: return self._attrs[name] except KeyError: return None
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
# Lib/tarfile.py:2198: try: g = grp.getgrnam(tarinfo.gname)[2] except KeyError: g = tarinfo.gid try: u = pwd.getpwnam(tarinfo.uname)[2] except KeyError: u = tarinfo.uid
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")
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
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()
try: (overrides[x] or default).ping() except IndexError: default.ping()
Narrowing of exception-catching scope -------------------------------------
The following examples, taken directly from Python's standard library, demonstrate how the scope of the try/except can be conveniently narrowed. To do this with the statement form of try/except would require a temporary variable, but it's far cleaner as an expression.
Lib/ipaddress.py:343:: try: ips.append(ip.ip) except AttributeError: ips.append(ip.network_address) Becomes:: ips.append(ip.ip except AttributeError: ip.network_address) The expression form is nearly equivalent to this:: try: _ = ip.ip except AttributeError: _ = ip.network_address ips.append(_)
Lib/tempfile.py:130:: try: dirlist.append(_os.getcwd()) except (AttributeError, OSError): dirlist.append(_os.curdir) Becomes:: dirlist.append(_os.getcwd() except (AttributeError, OSError): _os.curdir)
Lib/asyncore.py:264:: try: status.append('%s:%d' % self.addr) except TypeError: status.append(repr(self.addr)) Becomes:: status.append('%s:%d' % self.addr except TypeError: repr(self.addr))
Comparisons with other languages ================================
(With thanks to Andrew Barnert for compiling this section.)
`Ruby's`__ "begin…rescue…rescue…else…ensure…end" is an expression (potentially with statements inside it). It has the equivalent of an "as" clause, and the equivalent of bare except. And it uses no punctuation or keyword between the bare except/exception class/exception class with as clause and the value. (And yes, it's ambiguous unless you understand Ruby's statement/expression rules.)
__ http://www.skorks.com/2009/09/ruby-exceptions-and-exception-handling/
::
x = begin computation() rescue MyException => e default(e) end; x = begin computation() rescue MyException default() end; x = begin computation() rescue default() end; x = begin computation() rescue MyException default() rescue OtherException other() end;
In terms of this PEP::
x = computation() except MyException as e default(e) x = computation() except MyException default(e) x = computation() except default(e) x = computation() except MyException default() except OtherException other()
`Erlang`__ has a try expression that looks like this::
__ http://erlang.org/doc/reference_manual/expressions.html#id79284
x = try computation() catch MyException:e -> default(e) end; x = try computation() catch MyException:e -> default(e); OtherException:e -> other(e) end;
The class and "as" name are mandatory, but you can use "_" for either. There's also an optional "when" guard on each, and a "throw" clause that you can catch, which I won't get into. To handle multiple exceptions, you just separate the clauses with semicolons, which I guess would map to commas in Python. So::
x = try computation() except MyException as e -> default(e) x = try computation() except MyException as e -> default(e), OtherException as e->other_default(e)
Erlang also has a "catch" expression, which, despite using the same keyword, is completely different, and you don't want to know about it.
The ML family has two different ways of dealing with this, "handle" and "try"; the difference between the two is that "try" pattern-matches the exception, which gives you the effect of multiple except clauses and as clauses. In either form, the handler clause is punctuated by "=>" in some dialects, "->" in others.
To avoid confusion, I'll write the function calls in Python style.
Here's `SML's`__ "handle":: __ http://www.cs.cmu.edu/~rwh/introsml/core/exceptions.htm
let x = computation() handle MyException => default();;
Here's `OCaml's`__ "try":: __ http://www2.lib.uchicago.edu/keith/ocaml-class/exceptions.html
let x = try computation() with MyException explanation -> default(explanation);;
let x = try computation() with
MyException(e) -> default(e) | MyOtherException() -> other_default() | (e) -> fallback(e);;
In terms of this PEP, these would be something like::
x = computation() except MyException => default() x = try computation() except MyException e -> default() x = (try computation() except MyException as e -> default(e) except MyOtherException -> other_default() except BaseException as e -> fallback(e))
Many ML-inspired but not-directly-related languages from academia mix things up, usually using more keywords and fewer symbols. So, the `Oz`__ would map to Python as:: __ http://mozart.github.io/mozart-v1/doc-1.4.0/tutorial/node5.html
x = try computation() catch MyException as e then default(e)
Many Lisp-derived languages, like `Clojure,`__ implement try/catch as special forms (if you don't know what that means, think function-like macros), so you write, effectively:: __ http://clojure.org/special_forms#Special%20Forms--(try%20expr*%20catch-claus... ?)
try(computation(), catch(MyException, explanation, default(explanation)))
try(computation(), catch(MyException, explanation, default(explanation)), catch(MyOtherException, explanation, other_default(explanation)))
In Common Lisp, this is done with a slightly clunkier `"handler-case" macro,`__ but the basic idea is the same.
__ http://clhs.lisp.se/Body/m_hand_1.htm
The Lisp style is, surprisingly, used by some languages that don't have macros, like Lua, where `xpcall`__ takes functions. Writing lambdas Python-style instead of Lua-style:: __ http://www.gammon.com.au/scripts/doc.php?lua=xpcall
x = xpcall(lambda: expression(), lambda e: default(e))
This actually returns (true, expression()) or (false, default(e)), but I think we can ignore that part.
Haskell is actually similar to Lua here (except that it's all done with monads, of course)::
x = do catch(lambda: expression(), lambda e: default(e))
You can write a pattern matching expression within the function to decide what to do with it; catching and re-raising exceptions you don't want is cheap enough to be idiomatic.
But Haskell infixing makes this nicer::
x = do expression() `catch` lambda: default() x = do expression() `catch` lambda e: default(e)
And that makes the parallel between the lambda colon and the except colon in the proposal much more obvious::
x = expression() except Exception: default() x = expression() except Exception as e: default(e)
`Tcl`__ has the other half of Lua's xpcall; catch is a function which returns true if an exception was caught, false otherwise, and you get the value out in other ways. And it's all built around the the implicit quote-and-exec that everything in Tcl is based on, making it even harder to describe in Python terms than Lisp macros, but something like:: __ http://wiki.tcl.tk/902
if {[ catch("computation()") "explanation"]} { default(explanation) }
`Smalltalk`__ is also somewhat hard to map to Python. The basic version would be:: __ http://smalltalk.gnu.org/wiki/exceptions
x := computation() on:MyException do:default()
… but that's basically Smalltalk's passing-arguments-with-colons syntax, not its exception-handling syntax.
Deferred sub-proposals ======================
Multiple except clauses -----------------------
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 )
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.
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.
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.)
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
(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
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), it is extremely *uncommon* in the cases which could logically be converted into the expression form. Its few uses can simply be left unchanged. Consequently, in the interests of simplicity, the 'as' clause is not included in this proposal. A subsequent Python version can add this without breaking any existing code, as 'as' is already a keyword.
One example where this could possibly be useful is Lib/imaplib.py:568:: try: typ, dat = self._simple_command('LOGOUT') except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]] This could become:: typ, dat = (self._simple_command('LOGOUT') except BaseException as e: ('NO', '%s: %s' % (type(e), e))) Or perhaps some other variation. This is hardly the most compelling use-case, but an intelligent look at this code could tidy it up significantly. In the absence of further examples showing any need of the exception object, I have opted to defer indefinitely the recommendation.
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.
Bare except having different meaning ------------------------------------
With several of the proposed syntaxes, omitting the exception type name would be easy and concise, and would be tempting. For convenience's sake, it might be advantageous to have a bare 'except' clause mean something more useful than "except BaseException". Proposals included having it catch Exception, or some specific set of "common exceptions" (subclasses of a new type called ExpressionError), or have it look for a tuple named ExpressionError in the current scope, with a built-in default such as (ValueError, UnicodeError, AttributeError, EOFError, IOError, OSError, LookupError, NameError, ZeroDivisionError). All of these were rejected, for severa reasons.
* First and foremost, consistency with the statement form of try/except would be broken. Just as a list comprehension or ternary if expression can be explained by "breaking it out" into its vertical statement form, an expression-except should be able to be explained by a relatively mechanical translation into a near-equivalent statement. Any form of syntax common to both should therefore have the same semantics in each, and above all should not have the subtle difference of catching more in one than the other, as it will tend to attract unnoticed bugs.
* Secondly, the set of appropriate exceptions to catch would itself be a huge point of contention. It would be impossible to predict exactly which exceptions would "make sense" to be caught; why bless some of them with convenient syntax and not others?
* And finally (this partly because the recommendation was that a bare except should be actively encouraged, once it was reduced to a "reasonable" set of exceptions), any situation where you catch an exception you don't expect to catch is an unnecessary bug magnet.
Consequently, the use of a bare 'except' is down to two possibilities: either it is syntactically forbidden in the expression form, or it is permitted with the exact same semantics as in the statement form (namely, that it catch BaseException and be unable to capture it with 'as').
Bare except clauses -------------------
PEP 8 rightly advises against the use of a bare 'except'. While it is syntactically legal in a statement, and for backward compatibility must remain so, there is little value in encouraging its use. In an expression except clause, "except:" is a SyntaxError; use the equivalent long-hand form "except BaseException:" instead. A future version of Python MAY choose to reinstate this, which can be done without breaking compatibility.
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 )
This is more compelling when one or both of the deferred sub-proposals of multiple except clauses and/or exception capturing is included. In their absence, the parentheses would be thus:: value = expr except ExceptionType: default value = expr (except ExceptionType: default)
The advantage is minimal, and the potential to confuse a reader into thinking the except clause is separate from the expression, or into thinking this is a function call, makes this non-compelling. The expression can, of course, be parenthesized if desired, as can the default:: value = (expr) except ExceptionType: (default)
Short-hand for "except: pass" -----------------------------
The following was been suggested as a similar short-hand, though not technically an expression::
statement except Exception: pass
try: statement except Exception: pass
For instance, a common use-case is attempting the removal of a file:: os.unlink(some_file) except OSError: pass
There is an equivalent already in Python 3.4, however, in contextlib:: from contextlib import suppress with suppress(OSError): os.unlink(some_file)
As this is already a single line (or two with a break after the colon), there is little need of new syntax and a confusion of statement vs expression to achieve this.
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: _______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/eliben%40gmail.com
_______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/brett%40python.org
On Fri, Feb 21, 2014 at 6:39 AM, Brett Cannon <brett@python.org> wrote:
On Fri, Feb 21, 2014 at 9:03 AM, Eli Bendersky <eliben@gmail.com> wrote:
On Thu, Feb 20, 2014 at 7:15 PM, Chris Angelico <rosuav@gmail.com> wrote:
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, 21-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.
Chris, while I also commend you for the comprehensive PEP, I'm -1 on the proposal, for two main reasons:
1. Many proposals suggest new syntax to gain some succinctness. Each has to be judged for its own merits, and in this case IMHO the cons eclipse the pros. I don't think this will save a lot of code in a typical well-structured program - maybe a few lines out of hundreds. On the other hand, it adds yet another syntax to remember and understand, which is not the Pythonic way.
2. Worse, this idea subverts exceptions to control flow, which is not only un-Pythonic but also against the accepted practices of programming in general. Here, the comparison to PEP 308 is misguided. PEP 308, whatever syntax it adds, still remains within the domain of normal control flow. PEP 463, OTOH, makes it deliberately easy to make exceptions part of non-exceptional code, encouraging very bad programming practices.
But we subsumed using exception for control flow long ago.
StopIteration is the most blatant, but when we added dict.get(), getattr()
with a default value, iter() with its default, etc. we made it very clear that while exceptions typically represent exceptional situations, they can also be used as an easy signal of something which we expect you to handle and easily recover from (and returning None or some other singleton doesn't make sense), and yet without explicit API support it takes a heavy-handed statement to deal with.
True, but at least you still have to explicitly try...except... which takes a toll on the code so isn't taken lightly. Adding except into expressions, I fear, will proliferate this usage much more. Eli
-Brett
Eli
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
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.
With some situations, an LBYL technique can be used (checking if some sequence has enough length before indexing into it, for instance). This is not safe in all cases, but as it is often convenient, programmers will be tempted to sacrifice the safety of EAFP in favour of the notational brevity of LBYL. Additionally, some LBYL techniques (eg involving getattr with three arguments) warp the code into looking like literal strings rather than attribute lookup, which can impact readability. A convenient EAFP notation solves all of this.
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: return default() value = except_(lambda: 1/x, ZeroDivisionError, lambda: 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 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 of the overall expression. If any exception is raised, exception_list 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, non-matching exceptions will propagate upward.
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.)
This ternary operator would be between lambda and if/else in precedence.
Consider this example of a two-level cache:: for key in sequence: x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key))) # do something with x
This cannot be rewritten as:: x = lvl1.get(key, lvl2.get(key, f(key)))
which, despite being shorter, defeats the purpose of the cache, as it must calculate a default value to pass to get(). The .get() version calculates backwards; the exception-testing version calculates forwards, as would be expected. The nearest useful equivalent would be:: x = lvl1.get(key) or lvl2.get(key) or f(key) which depends on the values being nonzero, as well as depending on the cache object supporting this functionality.
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 default # Catches anything value = expr except(Exception) default # Catches only the named type(s) value = default if expr raise Exception value = expr or else default if Exception value = 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'. Also, in the context of the "default if expr raise Exception" proposal, it was suggested that a new keyword "raises" be used.
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.
Open Issues ===========
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
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.
A number of these examples are taken directly from the Python standard library, with file names and line numbers correct as of early Feb 2014. Many of these patterns are extremely common.
Retrieve an argument, defaulting to None:: cond = args[1] except IndexError: None
# Lib/pdb.py:803: try: cond = args[1] except IndexError: cond = None
Fetch information from the system if available:: pwd = os.getcwd() except OSError: None
# Lib/tkinter/filedialog.py:210: try: pwd = os.getcwd() except OSError: pwd = None
Attempt a translation, falling back on the original:: e.widget = self._nametowidget(W) except KeyError: W
# Lib/tkinter/__init__.py:1222: try: e.widget = self._nametowidget(W) except KeyError: e.widget = W
Read from an iterator, continuing with blank lines once it's exhausted:: line = readline() except StopIteration: ''
# Lib/lib2to3/pgen2/tokenize.py:370: try: line = readline() except StopIteration: line = ''
Retrieve platform-specific information (note the DRY improvement); 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: ''
# Lib/sysconfig.py:529: try: _CONFIG_VARS['abiflags'] = sys.abiflags except AttributeError: # sys.abiflags may not be defined on all platforms. _CONFIG_VARS['abiflags'] = ''
Retrieve an indexed item, defaulting to None (similar to dict.get):: def getNamedItem(self, name): return self._attrs[name] except KeyError: None
# Lib/xml/dom/minidom.py:573: def getNamedItem(self, name): try: return self._attrs[name] except KeyError: return None
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
# Lib/tarfile.py:2198: try: g = grp.getgrnam(tarinfo.gname)[2] except KeyError: g = tarinfo.gid try: u = pwd.getpwnam(tarinfo.uname)[2] except KeyError: u = tarinfo.uid
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")
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
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()
try: (overrides[x] or default).ping() except IndexError: default.ping()
Narrowing of exception-catching scope -------------------------------------
The following examples, taken directly from Python's standard library, demonstrate how the scope of the try/except can be conveniently narrowed. To do this with the statement form of try/except would require a temporary variable, but it's far cleaner as an expression.
Lib/ipaddress.py:343:: try: ips.append(ip.ip) except AttributeError: ips.append(ip.network_address) Becomes:: ips.append(ip.ip except AttributeError: ip.network_address) The expression form is nearly equivalent to this:: try: _ = ip.ip except AttributeError: _ = ip.network_address ips.append(_)
Lib/tempfile.py:130:: try: dirlist.append(_os.getcwd()) except (AttributeError, OSError): dirlist.append(_os.curdir) Becomes:: dirlist.append(_os.getcwd() except (AttributeError, OSError): _os.curdir)
Lib/asyncore.py:264:: try: status.append('%s:%d' % self.addr) except TypeError: status.append(repr(self.addr)) Becomes:: status.append('%s:%d' % self.addr except TypeError: repr(self.addr))
Comparisons with other languages ================================
(With thanks to Andrew Barnert for compiling this section.)
`Ruby's`__ "begin...rescue...rescue...else...ensure...end" is an expression (potentially with statements inside it). It has the equivalent of an "as" clause, and the equivalent of bare except. And it uses no punctuation or keyword between the bare except/exception class/exception class with as clause and the value. (And yes, it's ambiguous unless you understand Ruby's statement/expression rules.)
__ http://www.skorks.com/2009/09/ruby-exceptions-and-exception-handling/
::
x = begin computation() rescue MyException => e default(e) end; x = begin computation() rescue MyException default() end; x = begin computation() rescue default() end; x = begin computation() rescue MyException default() rescue OtherException other() end;
In terms of this PEP::
x = computation() except MyException as e default(e) x = computation() except MyException default(e) x = computation() except default(e) x = computation() except MyException default() except OtherException other()
`Erlang`__ has a try expression that looks like this::
__ http://erlang.org/doc/reference_manual/expressions.html#id79284
x = try computation() catch MyException:e -> default(e) end; x = try computation() catch MyException:e -> default(e); OtherException:e -> other(e) end;
The class and "as" name are mandatory, but you can use "_" for either. There's also an optional "when" guard on each, and a "throw" clause that you can catch, which I won't get into. To handle multiple exceptions, you just separate the clauses with semicolons, which I guess would map to commas in Python. So::
x = try computation() except MyException as e -> default(e) x = try computation() except MyException as e -> default(e), OtherException as e->other_default(e)
Erlang also has a "catch" expression, which, despite using the same keyword, is completely different, and you don't want to know about it.
The ML family has two different ways of dealing with this, "handle" and "try"; the difference between the two is that "try" pattern-matches the exception, which gives you the effect of multiple except clauses and as clauses. In either form, the handler clause is punctuated by "=>" in some dialects, "->" in others.
To avoid confusion, I'll write the function calls in Python style.
Here's `SML's`__ "handle":: __ http://www.cs.cmu.edu/~rwh/introsml/core/exceptions.htm
let x = computation() handle MyException => default();;
Here's `OCaml's`__ "try":: __ http://www2.lib.uchicago.edu/keith/ocaml-class/exceptions.html
let x = try computation() with MyException explanation -> default(explanation);;
let x = try computation() with
MyException(e) -> default(e) | MyOtherException() -> other_default() | (e) -> fallback(e);;
In terms of this PEP, these would be something like::
x = computation() except MyException => default() x = try computation() except MyException e -> default() x = (try computation() except MyException as e -> default(e) except MyOtherException -> other_default() except BaseException as e -> fallback(e))
Many ML-inspired but not-directly-related languages from academia mix things up, usually using more keywords and fewer symbols. So, the `Oz`__ would map to Python as:: __ http://mozart.github.io/mozart-v1/doc-1.4.0/tutorial/node5.html
x = try computation() catch MyException as e then default(e)
Many Lisp-derived languages, like `Clojure,`__ implement try/catch as special forms (if you don't know what that means, think function-like macros), so you write, effectively:: __ http://clojure.org/special_forms#Special%20Forms--(try%20expr*%20catch-claus... ?)
try(computation(), catch(MyException, explanation, default(explanation)))
try(computation(), catch(MyException, explanation, default(explanation)), catch(MyOtherException, explanation, other_default(explanation)))
In Common Lisp, this is done with a slightly clunkier `"handler-case" macro,`__ but the basic idea is the same.
__ http://clhs.lisp.se/Body/m_hand_1.htm
The Lisp style is, surprisingly, used by some languages that don't have macros, like Lua, where `xpcall`__ takes functions. Writing lambdas Python-style instead of Lua-style:: __ http://www.gammon.com.au/scripts/doc.php?lua=xpcall
x = xpcall(lambda: expression(), lambda e: default(e))
This actually returns (true, expression()) or (false, default(e)), but I think we can ignore that part.
Haskell is actually similar to Lua here (except that it's all done with monads, of course)::
x = do catch(lambda: expression(), lambda e: default(e))
You can write a pattern matching expression within the function to decide what to do with it; catching and re-raising exceptions you don't want is cheap enough to be idiomatic.
But Haskell infixing makes this nicer::
x = do expression() `catch` lambda: default() x = do expression() `catch` lambda e: default(e)
And that makes the parallel between the lambda colon and the except colon in the proposal much more obvious::
x = expression() except Exception: default() x = expression() except Exception as e: default(e)
`Tcl`__ has the other half of Lua's xpcall; catch is a function which returns true if an exception was caught, false otherwise, and you get the value out in other ways. And it's all built around the the implicit quote-and-exec that everything in Tcl is based on, making it even harder to describe in Python terms than Lisp macros, but something like:: __ http://wiki.tcl.tk/902
if {[ catch("computation()") "explanation"]} { default(explanation) }
`Smalltalk`__ is also somewhat hard to map to Python. The basic version would be:: __ http://smalltalk.gnu.org/wiki/exceptions
x := computation() on:MyException do:default()
... but that's basically Smalltalk's passing-arguments-with-colons syntax, not its exception-handling syntax.
Deferred sub-proposals ======================
Multiple except clauses -----------------------
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 )
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.
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.
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.)
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
(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
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), it is extremely *uncommon* in the cases which could logically be converted into the expression form. Its few uses can simply be left unchanged. Consequently, in the interests of simplicity, the 'as' clause is not included in this proposal. A subsequent Python version can add this without breaking any existing code, as 'as' is already a keyword.
One example where this could possibly be useful is Lib/imaplib.py:568:: try: typ, dat = self._simple_command('LOGOUT') except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]] This could become:: typ, dat = (self._simple_command('LOGOUT') except BaseException as e: ('NO', '%s: %s' % (type(e), e))) Or perhaps some other variation. This is hardly the most compelling use-case, but an intelligent look at this code could tidy it up significantly. In the absence of further examples showing any need of the exception object, I have opted to defer indefinitely the recommendation.
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.
Bare except having different meaning ------------------------------------
With several of the proposed syntaxes, omitting the exception type name would be easy and concise, and would be tempting. For convenience's sake, it might be advantageous to have a bare 'except' clause mean something more useful than "except BaseException". Proposals included having it catch Exception, or some specific set of "common exceptions" (subclasses of a new type called ExpressionError), or have it look for a tuple named ExpressionError in the current scope, with a built-in default such as (ValueError, UnicodeError, AttributeError, EOFError, IOError, OSError, LookupError, NameError, ZeroDivisionError). All of these were rejected, for severa reasons.
* First and foremost, consistency with the statement form of try/except would be broken. Just as a list comprehension or ternary if expression can be explained by "breaking it out" into its vertical statement form, an expression-except should be able to be explained by a relatively mechanical translation into a near-equivalent statement. Any form of syntax common to both should therefore have the same semantics in each, and above all should not have the subtle difference of catching more in one than the other, as it will tend to attract unnoticed bugs.
* Secondly, the set of appropriate exceptions to catch would itself be a huge point of contention. It would be impossible to predict exactly which exceptions would "make sense" to be caught; why bless some of them with convenient syntax and not others?
* And finally (this partly because the recommendation was that a bare except should be actively encouraged, once it was reduced to a "reasonable" set of exceptions), any situation where you catch an exception you don't expect to catch is an unnecessary bug magnet.
Consequently, the use of a bare 'except' is down to two possibilities: either it is syntactically forbidden in the expression form, or it is permitted with the exact same semantics as in the statement form (namely, that it catch BaseException and be unable to capture it with 'as').
Bare except clauses -------------------
PEP 8 rightly advises against the use of a bare 'except'. While it is syntactically legal in a statement, and for backward compatibility must remain so, there is little value in encouraging its use. In an expression except clause, "except:" is a SyntaxError; use the equivalent long-hand form "except BaseException:" instead. A future version of Python MAY choose to reinstate this, which can be done without breaking compatibility.
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 )
This is more compelling when one or both of the deferred sub-proposals of multiple except clauses and/or exception capturing is included. In their absence, the parentheses would be thus:: value = expr except ExceptionType: default value = expr (except ExceptionType: default)
The advantage is minimal, and the potential to confuse a reader into thinking the except clause is separate from the expression, or into thinking this is a function call, makes this non-compelling. The expression can, of course, be parenthesized if desired, as can the default:: value = (expr) except ExceptionType: (default)
Short-hand for "except: pass" -----------------------------
The following was been suggested as a similar short-hand, though not technically an expression::
statement except Exception: pass
try: statement except Exception: pass
For instance, a common use-case is attempting the removal of a file:: os.unlink(some_file) except OSError: pass
There is an equivalent already in Python 3.4, however, in contextlib:: from contextlib import suppress with suppress(OSError): os.unlink(some_file)
As this is already a single line (or two with a break after the colon), there is little need of new syntax and a confusion of statement vs expression to achieve this.
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: _______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/eliben%40gmail.com
_______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/brett%40python.org
On 22 February 2014 00:37, Eli Bendersky <eliben@gmail.com> wrote:
This goes against anything I understand about how exceptions should and should not be used.
I think you're thinking of a language that isn't Python - we use exceptions for control flow all over the place (it's how hasattr() is *defined*, for example). Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Fri, Feb 21, 2014 at 6:46 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
On 22 February 2014 00:37, Eli Bendersky <eliben@gmail.com> wrote:
This goes against anything I understand about how exceptions should and should not be used.
I think you're thinking of a language that isn't Python - we use exceptions for control flow all over the place (it's how hasattr() is *defined*, for example).
No, it is Python I'm thinking about. As I mentioned in the reply to Brett's message, I see a difference between allowing exceptions on expression level and statement level. The latter is less likely to be abused. Once you add exceptions into expressions, all bets are off. For instance, it is sometime non-trivial to know which exceptions some function may throw. When you write a try...raise statement, you think hard about covering all the bases. In an expression you're unlikely to, which opens up a lot of possibilities for bugs. Again, please stop focusing just on the list[index] case -- the proposal adds a new significant feature to the language that can (and thus will) be used in a variety of scenarios. Eli
On Sat, 22 Feb 2014 00:28:01 +1000 Nick Coghlan <ncoghlan@gmail.com> wrote:
Neither of these objections addresses the problems with the status quo, though:
- the status quo encourages overbroad exception handling (as illustrated by examples in the PEP)
I don't get this. Using the proper exception class in a "try...except" suite is no more bothersome than using the proper exception class in this newly-proposed construct. Regards Antoine.
On 22 February 2014 00:44, Eli Bendersky <eliben@gmail.com> wrote:
True, but at least you still have to explicitly try...except... which takes a toll on the code so isn't taken lightly. Adding except into expressions, I fear, will proliferate this usage much more.
The same fears were raised regarding conditional expressions, and they'll be dealt with the same way: style guides and code review. That's also why the examples part of the PEP is so important, though: this is about providing a tool to *improve* readability in the cases where it applies, without significantly increasing the cognitive burden of the language. One example not mentioned in the PEP is that "text.find(substr)" would become just a shorthand for "text.index(substr) except ValueError: -1", but the latter has the benefit that you can substitute "None" instead, which avoids the trap where "-1" is a valid subscript for the string (but doesn't give you the answer you want). Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Sat, Feb 22, 2014 at 12:53 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
On 21 February 2014 22:42, Chris Angelico <rosuav@gmail.com> wrote:
People can already write:
if (x if y else z):
without the parens, and it works. Readability suffers when the same keyword is used twice (here "if" rather than the colon, but same difference), yet the parens are considered optional. Python is a language that, by and large, lacks syntactic salt; style guides are free to stipulate more, but the language doesn't make demands. I would strongly *recommend* using parens in all the cases you've shown, especially lambda:
lambda x: calculate(x) except Exception: None lambda x: (calculate(x) except Exception: None)
as it would otherwise depend on operator precedence; but mandating them feels to me like demanding readability.
Right, that's why my main motivation for this suggestion is the one relating to keeping future options open. If the parentheses are optional, than adding multiple except clauses latter isn't possible, since this would already be valid, but mean something different:
expr except Exception1: default1 except Exception2: default2
The deferral currently has this snippet:
"""In order to ensure compatibility with future versions, ensure that any consecutive except operators are parenthesized to guarantee the interpretation you expect."""
That's not a reasonable expectation - either the parentheses have to be mandatory as part of the deferral, or else multiple except clause support needs to be listed as rejected rather than deferred.
I've spent the better part of the last hour debating this in my head. It's basically a question of simplicity versus future flexibility: either keep the syntax clean and deny the multiple-except-clause option, or mandate the parens and permit it. The first option has, in my own head, the stronger case - this is designed for simplicity, and it wouldn't be that big a deal to completely reject multiple except clauses and simply require that the
On 22 February 2014 00:50, Antoine Pitrou <solipsis@pitrou.net> wrote:
On Sat, 22 Feb 2014 00:28:01 +1000 Nick Coghlan <ncoghlan@gmail.com> wrote:
Neither of these objections addresses the problems with the status quo, though:
- the status quo encourages overbroad exception handling (as illustrated by examples in the PEP)
I don't get this. Using the proper exception class in a "try...except" suite is no more bothersome than using the proper exception class in this newly-proposed construct.
Not overbroad in the sense of catching too many different kinds of exception, overbroad in the sense of covering too much code in the try block. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Sat, Feb 22, 2014 at 1:59 AM, Chris Angelico <rosuav@gmail.com> wrote:
I've spent the better part of the last hour debating this in my head. It's basically a question of simplicity versus future flexibility: either keep the syntax clean and deny the multiple-except-clause option, or mandate the parens and permit it. The first option has, in my own head, the stronger case - this is designed for simplicity, and it wouldn't be that big a deal to completely reject multiple except clauses and simply require that the
Oops, hit the wrong key and sent that half-written. ... and simply require that the statement form be used. But the whelming opinion of python-dev seems to be in favour of the parens anyway, and since they give us the possibility of future expansion effectively for free, I've gone that way. Parens are now required; the syntax is: value = (expr except Exception: default) and, as per genexp rules, redundant parens can be omitted: print(lst[i] except IndexError: "Out of bounds") ChrisA
On 22 February 2014 00:59, Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Feb 22, 2014 at 12:53 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
The deferral currently has this snippet:
"""In order to ensure compatibility with future versions, ensure that any consecutive except operators are parenthesized to guarantee the interpretation you expect."""
That's not a reasonable expectation - either the parentheses have to be mandatory as part of the deferral, or else multiple except clause support needs to be listed as rejected rather than deferred.
I've spent the better part of the last hour debating this in my head. It's basically a question of simplicity versus future flexibility: either keep the syntax clean and deny the multiple-except-clause option, or mandate the parens and permit it. The first option has, in my own head, the stronger case - this is designed for simplicity, and it wouldn't be that big a deal to completely reject multiple except clauses and simply require that the
Yep, moving multiple exceptions to the "Rejected subproposals" section would work for me. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Sat, Feb 22, 2014 at 1:50 AM, Antoine Pitrou <solipsis@pitrou.net> wrote:
On Sat, 22 Feb 2014 00:28:01 +1000 Nick Coghlan <ncoghlan@gmail.com> wrote:
Neither of these objections addresses the problems with the status quo, though:
- the status quo encourages overbroad exception handling (as illustrated by examples in the PEP)
I don't get this. Using the proper exception class in a "try...except" suite is no more bothersome than using the proper exception class in this newly-proposed construct.
Overbroad exception handling comes in two ways. One is simply catching Exception or BaseException when a narrower class would be better, and that's not addressed by this PEP (except insofar as it does not have a bare "except:" syntax, and so it forces you to at least be explicit about catching BaseException). The other is this: try: f(x[i]) except IndexError: f(default) Translating that into this form: f(x[i] except IndexError: default) means that an IndexError thrown inside f() will not be caught. While it is, of course, possible to write that currently: try: arg = x[i] except IndexError: arg = default f(arg) it's that much less readable, and as evidenced by several examples in the standard library (some of which are in the PEP, and a few more can be found in my associated collection [1]), it's common to short-cut this. By providing a clean and convenient syntax for catching an exception in just one small expression, we encourage the narrowing of the catch front. ChrisA [1] https://github.com/Rosuav/ExceptExpr/blob/master/examples.py
On Fri, Feb 21, 2014 at 9:49 AM, Eli Bendersky <eliben@gmail.com> wrote:
On Fri, Feb 21, 2014 at 6:46 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
On 22 February 2014 00:37, Eli Bendersky <eliben@gmail.com> wrote:
This goes against anything I understand about how exceptions should and should not be used.
I think you're thinking of a language that isn't Python - we use exceptions for control flow all over the place (it's how hasattr() is *defined*, for example).
No, it is Python I'm thinking about. As I mentioned in the reply to Brett's message, I see a difference between allowing exceptions on expression level and statement level. The latter is less likely to be abused. Once you add exceptions into expressions, all bets are off.
For instance, it is sometime non-trivial to know which exceptions some function may throw. When you write a try...raise statement, you think hard about covering all the bases. In an expression you're unlikely to, which opens up a lot of possibilities for bugs. Again, please stop focusing just on the list[index] case -- the proposal adds a new significant feature to the language that can (and thus will) be used in a variety of scenarios.
I understand you are arguing that a try expression will lead to people just doing `something() except Exception: None` or whatever and that people will simply get lazy and not think about what they are doing with their exceptions. Unfortunately they already are; that shipped sailed when we didn't eliminate bare except clauses in Python 3 (hopefully we can change that in Python 4). I personally don't think that making people type out a try statement is going to lead to that much thought compared to an expression. I'm willing to bet most IDEs have a code snippet for creating a try statement so people are already not using the extra typing of a full-blown statement with at least two clauses as a way to stop and think about what they are doing. I'm totally fine restricting this proposal to not having any concept of exception catching or finally clause: it just replaces the simplest try/except clause possible (while requiring an exception be specified). That takes care of the common control flow use case of exceptions while requiring more thought for more complex cases.
On Sat, Feb 22, 2014 at 1:03 AM, Eli Bendersky <eliben@gmail.com> wrote:
Chris, while I also commend you for the comprehensive PEP, I'm -1 on the proposal, for two main reasons:
1. Many proposals suggest new syntax to gain some succinctness. Each has to be judged for its own merits, and in this case IMHO the cons eclipse the pros. I don't think this will save a lot of code in a typical well-structured program - maybe a few lines out of hundreds. On the other hand, it adds yet another syntax to remember and understand, which is not the Pythonic way.
It doesn't need to save a huge number of lines. Just like lambda and the if/else expression, it's there as a tool - if it makes your code easier to read, it's a good too, and if it makes it harder, then don't use it. Yes, it's more to learn, but so is the proliferation of ad-hoc alternatives, several of which are listed in the 'Motivation' section at the top.
2. Worse, this idea subverts exceptions to control flow, which is not only un-Pythonic but also against the accepted practices of programming in general. Here, the comparison to PEP 308 is misguided. PEP 308, whatever syntax it adds, still remains within the domain of normal control flow. PEP 463, OTOH, makes it deliberately easy to make exceptions part of non-exceptional code, encouraging very bad programming practices.
Again, I point to the current alternatives, including: * dict[key] -> dict.get(key, default) * min(sequence) -> min(sequence, default=default), as of 3.4 Both offer a way to either get an exception back or use a (usually constant) default value. In each case, the author of the class/function had to cater to the fact that some callers might want it to not raise an exception. The alternative is to always use the full try/except block, which leads to questions like "How can I keep going after an exception?" with code like this: try: spam = d["spam"] ham = d["ham"] eggs = d["eggs"] sausage = d["sausage"] except KeyError: thing_that_failed = "" The dict class offers a way to avoid the exception here, by showing that it's non-exceptional: spam = d.get("spam","") ham = d.get("ham","") eggs = d.get("eggs","") sausage = d.get("sausage","") But not everything does. Writing that with just exception handling looks like this: try: spam = d["spam"] except KeyError: span = "" try: ham = d["ham"] except KeyError: ham = "" try: eggs = d["eggs"] except KeyError: eggs = "" try: sausage = d["sausage"] except KeyError: sausage = "" with extreme likelihood of an error - do you see what I got wrong there? With inline exception handling, d could be a custom class that simply defines [...] to raise KeyError on unknowns, and the code can be written thus: spam = d["spam"] except KeyError: "" ham = d["ham"] except KeyError: "" eggs = d["eggs"] except KeyError: "" sausage = d["sausage"] except KeyError: "" It's still a bit repetitive, but that's hard to avoid. And it puts the exception handling at the exact point where it stops being exceptional and starts being normal - exactly as the try/except statement should. ChrisA
On Sat, Feb 22, 2014 at 1:22 AM, Rob Cliffe <rob.cliffe@btinternet.com> wrote:
Small point: in one of your examples you give a plug for the PEP "note the DRY improvement". I would suggest that similarly perhaps in your Lib/tarfile.py:2198 example you point out the increase in readability due to the 2 lines lining up in your Lib/ipaddress.py:343 example you point out that the new form is probably an improvement (i.e. probably closer to the author's intention) as it will NOT catch an AttributeError evaluating "ips.append" (this would matter e.g. if "append" were mis-spelt). YOU are clearly aware of this but it would often escape the casual reader.
Sure. Added a paragraph down the bottom of the section explaining the benefit of the narrowed scope. ChrisA
On Sat, Feb 22, 2014 at 1:34 AM, Brett Cannon <brett@python.org> wrote:
While I like the general concept, I agree that it looks too much like a crunched statement; the use of the colon is a non-starter for me. I'm sure I'm not the only one whose brain has been trained to view a colon in Python to mean "statement", period. This goes against that syntactic practice and just doesn't work for me.
I'm -1 with the current syntax, but it can go into the + range if a better syntax can be chosen.
We bikeshedded that extensively on -ideas. The four best options are: value = (expr except Exception: default) value = (expr except Exception -> default) value = (expr except Exception pass default) value = (expr except Exception then default) Note that the last option involves the creation of a new keyword. Would any of the others feel better to you? ChrisA
On Sat, Feb 22, 2014 at 2:19 AM, Brett Cannon <brett@python.org> wrote:
I understand you are arguing that a try expression will lead to people just doing `something() except Exception: None` or whatever and that people will simply get lazy and not think about what they are doing with their exceptions. Unfortunately they already are; that shipped sailed when we didn't eliminate bare except clauses in Python 3 (hopefully we can change that in Python 4).
That's already come up a few times. I wrote a script to try to find potential conversion targets, and technically it's correct to show this up: Doc/tools/rstlint.py:73: try: compile(code, fn, 'exec') except SyntaxError as err: yield err.lineno, 'not compilable: %s' % err Yes, technically you could treat that as an expression. Well, okay. Current version of the proposal doesn't include 'as', so you can't do that. Let's grab the next one. Lib/asyncio/events.py:40: try: self._callback(*self._args) except Exception: logger.exception('Exception in callback %s %r', self._callback, self._args) That could, per PEP 463, become: (self._callback(*self._args) except Exception: logger.exception('Exception in callback %s %r', self._callback, self._args)) But that would be abuse of the syntax. It's just like converting this: for s in list_of_strings: print(s) into an expression: [print(s) for s in list_of_strings] list(map(print,list_of_strings)) Neither of them is a good way to write code, yet that's not the fault of either map() or the list comp.
I'm totally fine restricting this proposal to not having any concept of exception catching or finally clause: it just replaces the simplest try/except clause possible (while requiring an exception be specified). That takes care of the common control flow use case of exceptions while requiring more thought for more complex cases.
The finally clause was never in the proposal. I added it as an open issue early on, just to see if it gathered any interest, but the weight of opinion agreed with my initial feeling: it's utterly useless to the exception syntax. Same goes for an else clause. Currently, the two complexities not possible in the expression form are capturing the exception with 'as' (for technical reasons) and stringing multiple except clauses on (for complexity reasons). In the standard library, counting up all the cases that could plausibly be converted to expression form, very very few use either feature. So this is a case of "do just the really simple option and catch 98.5% of the actual usage". ChrisA
On Sat, 22 Feb 2014 02:12:04 +1100 Chris Angelico <rosuav@gmail.com> wrote:
Overbroad exception handling comes in two ways. One is simply catching Exception or BaseException when a narrower class would be better, and that's not addressed by this PEP (except insofar as it does not have a bare "except:" syntax, and so it forces you to at least be explicit about catching BaseException). The other is this:
try: f(x[i]) except IndexError: f(default)
Translating that into this form:
f(x[i] except IndexError: default)
means that an IndexError thrown inside f() will not be caught.
Thank you and Nick for the explanation. This is a good point. I'm still put off by the syntax, though, just like Brett. Regards Antoine.
[re: "as" clause] On Fri, Feb 21, 2014 at 6:20 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
It's probably OK to leave it in the deferred section and just note the difficulty of implementing it in a backwards compatible way, since we're *not* going to be introducing a closure.
Agreed. -eric
On Fri, Feb 21, 2014 at 8:12 AM, Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Feb 22, 2014 at 1:50 AM, Antoine Pitrou <solipsis@pitrou.net> wrote:
On Sat, 22 Feb 2014 00:28:01 +1000 Nick Coghlan <ncoghlan@gmail.com> wrote:
Neither of these objections addresses the problems with the status quo, though:
- the status quo encourages overbroad exception handling (as illustrated by examples in the PEP)
I don't get this. Using the proper exception class in a "try...except" suite is no more bothersome than using the proper exception class in this newly-proposed construct.
Overbroad exception handling comes in two ways. One is simply catching Exception or BaseException when a narrower class would be better, and that's not addressed by this PEP (except insofar as it does not have a bare "except:" syntax, and so it forces you to at least be explicit about catching BaseException). The other is this:
try: f(x[i]) except IndexError: f(default)
Translating that into this form:
f(x[i] except IndexError: default)
means that an IndexError thrown inside f() will not be caught. While it is, of course, possible to write that currently:
try: arg = x[i] except IndexError: arg = default f(arg)
it's that much less readable, and as evidenced by several examples in the standard library (some of which are in the PEP, and a few more can be found in my associated collection [1]), it's common to short-cut this. By providing a clean and convenient syntax for catching an exception in just one small expression, we encourage the narrowing of the catch front.
Be sure to capture in the PEP (within reason) a summary of concerns and rebuttals/acquiescence. Eli's, Brett's, and Antoine's concerns likely reflect what others are thinking as well. The PEP and its result will be better for recording such matters. In fact, that's a key goal of PEPs. :) -eric
On Fri, Feb 21, 2014 at 8:19 AM, Brett Cannon <brett@python.org> wrote:
I understand you are arguing that a try expression will lead to people just doing `something() except Exception: None` or whatever and that people will simply get lazy and not think about what they are doing with their exceptions.
Exactly.
Unfortunately they already are; that shipped sailed when we didn't eliminate bare except clauses in Python 3 (hopefully we can change that in Python 4).
We should have a page somewhere tracking a py4k "wishlist". :)
I personally don't think that making people type out a try statement is going to lead to that much thought compared to an expression. I'm willing to bet most IDEs have a code snippet for creating a try statement so people are already not using the extra typing of a full-blown statement with at least two clauses as a way to stop and think about what they are doing.
I'm totally fine restricting this proposal to not having any concept of exception catching or finally clause: it just replaces the simplest try/except clause possible (while requiring an exception be specified). That takes care of the common control flow use case of exceptions while requiring more thought for more complex cases.
+1 -eric
On Fri, Feb 21, 2014 at 8:27 AM, Chris Angelico <rosuav@gmail.com> wrote:
It doesn't need to save a huge number of lines. Just like lambda and the if/else expression, it's there as a tool - if it makes your code easier to read, it's a good too, and if it makes it harder, then don't use it.
And as long as the tool/syntax is obviously linked to an existing one (and does not change semantics), people really won't need to learn anything new. So it wouldn't be adding much to the keep-it-in-my-head size of the language. -eric
On Sat, Feb 22, 2014 at 3:46 AM, Eric Snow <ericsnowcurrently@gmail.com> wrote:
Be sure to capture in the PEP (within reason) a summary of concerns and rebuttals/acquiescence. Eli's, Brett's, and Antoine's concerns likely reflect what others are thinking as well. The PEP and its result will be better for recording such matters. In fact, that's a key goal of PEPs. :)
I now have a paragraph summarizing the "narrowing of scope" benefit, though not worded as a rebuttal of a concern. Eli's concern is mainly that this can be abused, which everything can; should I include something about that? Brett's issue is with the colon. I'll write up a paragraph or two of reasoning as to why that was selected, possibly tomorrow, but the main reason is simply that nobody came up with a compelling argument in favour of any other. Hardly the most epic tale :) ChrisA
On Sat, 22 Feb 2014 02:52:59 +1100 Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Feb 22, 2014 at 1:34 AM, Brett Cannon <brett@python.org> wrote:
While I like the general concept, I agree that it looks too much like a crunched statement; the use of the colon is a non-starter for me. I'm sure I'm not the only one whose brain has been trained to view a colon in Python to mean "statement", period. This goes against that syntactic practice and just doesn't work for me.
I'm -1 with the current syntax, but it can go into the + range if a better syntax can be chosen.
We bikeshedded that extensively on -ideas. The four best options are:
value = (expr except Exception: default)
-0.5
value = (expr except Exception -> default)
-0.5
value = (expr except Exception pass default)
-1 (looks terribly weird)
value = (expr except Exception then default)
+0.5 But I'm aware it requires reserving "then" as a keyword, which might need a prior SyntaxWarning. Regards Antoine.
On Sat, Feb 22, 2014 at 4:32 AM, Antoine Pitrou <solipsis@pitrou.net> wrote:
value = (expr except Exception then default)
+0.5 But I'm aware it requires reserving "then" as a keyword, which might need a prior SyntaxWarning.
There are no instances of "then" used as a name in the Python stdlib, I already checked. If anyone has a good-sized codebase to check, you're welcome to use the same analysis script - it's an AST parser/walker, so it finds actual use as a name, not in a comment or a string or anything. https://github.com/Rosuav/ExceptExpr/blob/master/find_except_expr.py ChrisA
On Fri, Feb 21, 2014 at 9:32 AM, Antoine Pitrou <solipsis@pitrou.net> wrote:
On Sat, 22 Feb 2014 02:52:59 +1100 Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Feb 22, 2014 at 1:34 AM, Brett Cannon <brett@python.org> wrote:
While I like the general concept, I agree that it looks too much like a crunched statement; the use of the colon is a non-starter for me. I'm sure I'm not the only one whose brain has been trained to view a colon in Python to mean "statement", period. This goes against that syntactic practice and just doesn't work for me.
I'm -1 with the current syntax, but it can go into the + range if a better syntax can be chosen.
We bikeshedded that extensively on -ideas. The four best options are:
value = (expr except Exception: default)
-0.5
value = (expr except Exception -> default)
-0.5
value = (expr except Exception pass default)
-1 (looks terribly weird)
value = (expr except Exception then default)
+0.5 But I'm aware it requires reserving "then" as a keyword, which might need a prior SyntaxWarning.
I'm put off by the ':' syntax myself (it looks to me as if someone forgot a newline somewhere) but 'then' feels even weirder (it's been hard-coded in my brain as meaning the first branch of an 'if'). I am going to sleep on this. -- --Guido van Rossum (python.org/~guido)
On 02/20/2014 07:15 PM, Chris Angelico wrote:
PEP: 463 Title: Exception-catching expressions
[snip]
Deferred sub-proposals ======================
Multiple except clauses -----------------------
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 )
-1 If one wants to catch multiple exceptions from the same original operation, use the statement block. Keep the exception expression syntax simple and chained, akin to the if..else syntax: value = expr1 except exc1 expr2 except exc2 expr3 ... So each following except is only catching exceptions from the last expression (in other words, exc1 only from expr1, exc2 only from expr2, etc.) Keep it simple. If one wants the power of the statement form, use the statement syntax. -- ~Ethan~
Thank you for writing this PEP, Chris. I'm impressed by the quality of this PEP, and how you handled the discussion on python-ideas. I initially liked this idea, however, after reading the PEP in detail, my vote is: -1 on the current syntax; -1 on the whole idea. On 2/20/2014, 10:15 PM, Chris Angelico wrote:
[snip] * 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 We can fix that in 3.5. * seq[index] - no way to handle a bounds error We can add 'list.get(index, default)' method, similar to 'Mapping.get'. It's far more easier than introducing new syntax.
* min(sequence, default=default) - keyword argument in place of ValueError
* sum(sequence, start=default) - slightly different but can do the same job 'sum' is not a very frequently used builtin.
I think the Motivation section is pretty weak. Inconvenience of dict[] raising KeyError was solved by introducing the dict.get() method. And I think that dct.get('a', 'b') is 1000 times better than dct['a'] except KeyError: 'b' I don't want to see this (or any other syntax) used by anyone. I also searched how many 'except IndexError' are in the standard library code. Around 60. That's a rather low number, that can justify adding 'list.get' but not advocate a new syntax. Moreover, I think that explicit handling of IndexError is rather ugly and error prone, using len() is usually reads better.
[snip]
Consider this example of a two-level cache:: for key in sequence: x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key))) # do something with x
I'm sorry, it took me a minute to understand what your example is doing. I would rather see two try..except blocks than this.
Retrieve an argument, defaulting to None:: cond = args[1] except IndexError: None
# Lib/pdb.py:803: try: cond = args[1] except IndexError: cond = None
Attempt a translation, falling back on the original:: e.widget = self._nametowidget(W) except KeyError: W
# Lib/tkinter/__init__.py:1222: try: e.widget = self._nametowidget(W) except KeyError: e.widget = W I'm not sure this is a good example either. I presume '_nametowidget' is some function,
cond = None if (len(args) < 2) else args[1] that might raise a KeyError because of a bug in its implementation, or to signify that there is no widget 'W'. Your new syntax just helps to work with this error prone api.
Read from an iterator, continuing with blank lines once it's exhausted:: line = readline() except StopIteration: ''
# Lib/lib2to3/pgen2/tokenize.py:370: try: line = readline() except StopIteration: line = ''
Handling StopIteration exception is more common in standard library than IndexError (although not much more), but again, not all of that code is suitable for your syntax. I'd say about 30%, which is about 20-30 spots (correct me if I'm wrong).
Retrieve platform-specific information (note the DRY improvement); 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: ''
# Lib/sysconfig.py:529: try: _CONFIG_VARS['abiflags'] = sys.abiflags except AttributeError: # sys.abiflags may not be defined on all platforms. _CONFIG_VARS['abiflags'] = ''
Retrieve an indexed item, defaulting to None (similar to dict.get):: def getNamedItem(self, name): return self._attrs[name] except KeyError: None
# Lib/xml/dom/minidom.py:573: def getNamedItem(self, name): try: return self._attrs[name] except KeyError: return None _attrs there is a dict (or at least it's something that quaks
Ugly. _CONFIG_VARS['abiflags'] = getattr(sys, 'abiflags', '') Much more readable. like a dict, and has [] and keys()), so return self._attrs.get(name)
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
# Lib/tarfile.py:2198: try: g = grp.getgrnam(tarinfo.gname)[2] except KeyError: g = tarinfo.gid try: u = pwd.getpwnam(tarinfo.uname)[2] except KeyError: u = tarinfo.uid This one is a valid example, but totally unparseable by humans. Moreover, it promotes a bad pattern, as you mask KeyErrors in 'grp.getgrnam(tarinfo.gname)' call.
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")
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
I think all of the above more readable with try statement.
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", _)
If you replace '_' with a 'msg' (why did you use '_'??) then try statements are *much* more readable.
[snip]
Lib/ipaddress.py:343:: try: ips.append(ip.ip) except AttributeError: ips.append(ip.network_address) Becomes:: ips.append(ip.ip except AttributeError: ip.network_address) or it may become:
ips.append(getattr(ip, 'ip', ip.network_address)) or address = getattr(ip, 'ip', ip.network_address) ips.append(address) --- All in all, your proposal scares me. I doesn't make python code readable, it doesn't solve the problem of overbroad exceptions handling (you have couple examples of overbroad handling in your PEP examples section). Yes, some examples look neat. But your syntax is much easier to abuse, than 'if..else' expression, and if people start abusing it, Python will simply loose it's readability advantage. Yury
On 22 February 2014 02:03, Chris Angelico <rosuav@gmail.com> wrote:
Oops, hit the wrong key and sent that half-written.
... and simply require that the statement form be used. But the whelming opinion of python-dev seems to be in favour of the parens anyway, and since they give us the possibility of future expansion effectively for free, I've gone that way. Parens are now required; the syntax is:
value = (expr except Exception: default)
Let me add my congratulations on a fine PEP. I think it's much more readable with the parens (doesn't look as much like there's a missing newline). I'd also strongly argue for permanently disallowing multiple exceptions - as you said, this is intended to be a simple, readable syntax. Even with the parens, I share the bad gut feeling others are having with the colon in the syntax. I also don't think "then" is a good fit (even if it didn't add a keyword). Unfortunately, I can't come up with anything better ... Tim Delaney
Nick Coghlan wrote:
On 21 February 2014 13:15, Chris Angelico <rosuav@gmail.com> wrote:
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
I'd like to make the case that the PEP should adopt this as its default position.
I generally agree, but I'd like to point out that this doesn't necessarily mean making the parenthesizing rules as strict as they are for generator expressions. The starting point for genexps is that the parens are part of the syntax, the same way that square brackets are part of the syntax of a list comprehension; we only allow them to be omitted in very special circumstances. On the other hand, I don't think there's any harm in allowing an except expression to stand on its own when there is no risk of ambiguity, e.g. foo = things[i] except IndexError: None should be allowed, just as we allow x = a if b else c and don't require x = (a if b else c) -- Greg
On 2/21/2014 5:06 PM, Greg Ewing wrote:
Nick Coghlan wrote:
On 21 February 2014 13:15, Chris Angelico <rosuav@gmail.com> wrote:
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
I'd like to make the case that the PEP should adopt this as its default position.
I generally agree, but I'd like to point out that this doesn't necessarily mean making the parenthesizing rules as strict as they are for generator expressions.
The starting point for genexps is that the parens are part of the syntax, the same way that square brackets are part of the syntax of a list comprehension; we only allow them to be omitted in very special circumstances.
On the other hand, I don't think there's any harm in allowing an except expression to stand on its own when there is no risk of ambiguity, e.g.
foo = things[i] except IndexError: None
I agree that it would be a shame to disallow this simple usage. I'd like to leave this like any other expression: add parens if they add clarity or if they are required for the precedence you'd prefer. Personally, I'd probably always use parens unless it was a simple assignment (as above), or an argument to a function call. But I think it's a style issue. Eric.
should be allowed, just as we allow
x = a if b else c
and don't require
x = (a if b else c)
Antoine Pitrou wrote:
lst = [1, 2] value = lst[2] except IndexError: "No value"
the gain in concision is counterbalanced by a loss in readability,
This version might be more readable: value = lst[2] except "No value" if IndexError since it puts the normal and exceptional values next to each other, and relegates the exception type (which is of much less interest) to a trailing aside. -- Greg
On 02/21/2014 02:26 PM, Eric V. Smith wrote:
On 2/21/2014 5:06 PM, Greg Ewing wrote:
On 21 February 2014 13:15, Chris Angelico wrote:
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
There would be no ambiguity if only nested excepts are allowed. If one wants to catch multiple exceptions from one expression, /and do something different for each one/, use the statement form as it's going to be clearer. For example: try: value = 1/x except ZeroDivisionError: try: value = 1/default['denominator'] except KeyError: value = NaN is much cleaner as: value = 1/x except ZeroDivisionError: 1/default['denominator'] except KeyError: NaN However, this: try: result = Parse(some_stuff) except MissingOperator: result = ... except InvalidOperand: result = ... except SomethingElse: result = ... would not benefit from being condensed into a single expression -- ~Ethan~
On 02/21/2014 03:29 PM, Greg Ewing wrote:
Antoine Pitrou wrote:
lst = [1, 2] value = lst[2] except IndexError: "No value"
the gain in concision is counterbalanced by a loss in readability,
This version might be more readable:
value = lst[2] except "No value" if IndexError
It does read nicely, and is fine for the single, non-nested, case (which is probably the vast majority), but how would it handle nested exceptions? -- ~Ethan~
Eli Bendersky wrote:
For instance, it is sometime non-trivial to know which exceptions some function may throw. When you write a try...raise statement, you think hard about covering all the bases. In an expression you're unlikely to,
Speak for yourself. I don't think I would put any less thought into which exception I caught with an except expression as I would for an except statement. In fact, an except expression may even make it easier to catch exceptions in an appropriately targeted way. For example, a pattern frequently encountered is: result = computation(int(arg)) and you want to guard against arg not being a well-formed int. It's tempting to do this: try: result = computation(int(arg)) except ValueError: abort("Invalid int") But that's bad, because the try clause encompasses too much. Doing it properly requires splitting up the expression: try: i = int(arg) except: abort("Invalid int") else: result = computation(i) With an except expression, it could be written: result = computation(int(arg) except ValueError: abort("Invalid int")) -- Greg
On 02/21/2014 11:04 AM, Yury Selivanov wrote:
On 2/20/2014, 10:15 PM, Chris Angelico wrote:
* list.pop() - no way to return a default
We can fix that in 3.5.
How many are you going to "fix"? How are you going to "fix" the routines you don't control?
* seq[index] - no way to handle a bounds error
We can add 'list.get(index, default)' method, similar to 'Mapping.get'. It's far more easier than introducing new syntax.
When I have to keep writing the same code over and over and aver again, I find a better way to do the job. In this case, an exception expression does quite nicely.
I also searched how many 'except IndexError' are in the standard library code. Around 60. That's a rather low number, that can justify adding 'list.get' but not advocate a new syntax.
And roughly 200 of KeyError, another couple hundred of ValueError... This is not just about better handling of [missing] default values, but of better exception handling. This PEP adds the ability to use a scalpel instead of a sledge hammer. -- ~Ethan~
On 22 February 2014 10:29, Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
Antoine Pitrou wrote:
lst = [1, 2]
value = lst[2] except IndexError: "No value"
the gain in concision is counterbalanced by a loss in readability,
This version might be more readable:
value = lst[2] except "No value" if IndexError
+1 - it's readable, clear, and only uses existing keywords. Tim Delaney
21.02.2014 18:37, Guido van Rossum wrote:
I'm put off by the ':' syntax myself (it looks to me as if someone forgot a newline somewhere)
As I mentioned at python-ideas I believe that parens neutralize, at least to some extent, that unfortunate statement-ish flavor of the colon. This one has some statement-like smell: msg = seq[i] except IndexError: "nothing" But this looks better, I believe: msg = (seq[i] except IndexError: "nothing") Or even (still being my favorite): msg = seq[i] except (IndexError: "nothing") Cheers. *j
On 2/21/2014 3:29 PM, Greg Ewing wrote:
Antoine Pitrou wrote:
lst = [1, 2] value = lst[2] except IndexError: "No value"
the gain in concision is counterbalanced by a loss in readability,
This version might be more readable:
value = lst[2] except "No value" if IndexError
since it puts the normal and exceptional values next to each other, and relegates the exception type (which is of much less interest) to a trailing aside.
Ternary if teaches us that the normal and alternate values should be on either end, so the present proposal corresponds to that; ternary if also teaches us that nesting affects the right value, in the absence of parentheses, this proposal seems to correspond, although requiring parentheses or not is presently a topic of discussion in a different subthread. On the other hand, "if" and "except" are opposites in another way: "if" is followed by an expression that, if true, means the left value is used, and if false, means the right value is used. "Except" is followed by an exception that, if thrown, means the right value is used, and if not thrown, means the left value is used... but the word "except" is opposite in meaning to "if", so this actually fits nicely with English definitions. Here's a challenge: There has been a big thread about None versus (SQL) Null. Show how an except: expression can help the DB API more easily convert from using None to using a new Null singleton, and you'll have a winner :)
On Sat, Feb 22, 2014 at 01:15:13PM +1300, Greg Ewing wrote:
Eli Bendersky wrote:
For instance, it is sometime non-trivial to know which exceptions some function may throw. When you write a try...raise statement, you think hard about covering all the bases. In an expression you're unlikely to,
Speak for yourself. I don't think I would put any less thought into which exception I caught with an except expression as I would for an except statement.
+1 With the possibly exception of messing about in the interactive interpreter, "catch as much as possible" is an anti-pattern. If you're not trying to catch as little as possible, you're doing it wrong.
In fact, an except expression may even make it easier to catch exceptions in an appropriately targeted way. For example, a pattern frequently encountered is:
result = computation(int(arg))
and you want to guard against arg not being a well-formed int. It's tempting to do this:
try: result = computation(int(arg)) except ValueError: abort("Invalid int")
But that's bad, because the try clause encompasses too much. Doing it properly requires splitting up the expression:
try: i = int(arg) except: abort("Invalid int") else: result = computation(i)
With an except expression, it could be written:
result = computation(int(arg) except ValueError: abort("Invalid int"))
Nice example! Except I'd lay the code out a bit better to emphasise which part is being guarded: result = computation( int(arg) except ValueError: abort("Invalid int") ) Actually, not quite so nice as I first thought, since you're relying on the side-effects of abort() rather than returning a value. But apart from that quibble, the ability to guard *small* subcalculations without needing a visually heavy try...block is a good use-case. -- Steven
On 02/21/2014 05:21 PM, Steven D'Aprano wrote:
On Sat, Feb 22, 2014 at 01:15:13PM +1300, Greg Ewing wrote:
With an except expression, it could be written:
result = computation(int(arg) except ValueError: abort("Invalid int"))
Nice example! Except I'd lay the code out a bit better to emphasise which part is being guarded:
result = computation( int(arg) except ValueError: abort("Invalid int") )
Actually, not quite so nice as I first thought, since you're relying on the side-effects of abort() rather than returning a value.
Think of it rather as transforming the exception to be more useful. -- ~Ethan~
Ethan Furman wrote:
On 02/21/2014 03:29 PM, Greg Ewing wrote:
value = lst[2] except "No value" if IndexError
It does read nicely, and is fine for the single, non-nested, case (which is probably the vast majority), but how would it handle nested exceptions?
Hmmm, probably not very well, unless we define a except b if E1 except c if E2 to mean a except (b except c if E2) if E1 If E1 == E2, that could perhaps be abbreviated to a except b except c if E Or we could just decide that the nested case is going to be so rare it's not worth worrying about. -- Greg
On 2/21/2014, 7:42 PM, Ethan Furman wrote:
On 02/21/2014 11:04 AM, Yury Selivanov wrote:
On 2/20/2014, 10:15 PM, Chris Angelico wrote:
* list.pop() - no way to return a default
We can fix that in 3.5.
How many are you going to "fix"? How are you going to "fix" the routines you don't control?
This new syntax won't magically fix all the code either. But it may let people write code like this: # I'm sorry, I really can't read this. 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") or this: # We happily mask exceptions from getgrnam g = grp.getgrnam(tarinfo.gname)[2] except KeyError: tarinfo.gid And this particular case or list.pop method, let's be honest, can be fixed ;)
* seq[index] - no way to handle a bounds error
We can add 'list.get(index, default)' method, similar to 'Mapping.get'. It's far more easier than introducing new syntax.
When I have to keep writing the same code over and over and aver again, I find a better way to do the job. In this case, an exception expression does quite nicely.
I can't believe you find list[42] except IndexError: 'spam' to be better than list.get(42, 'spam') If IndexError is such a big deal, I think we can add this function to list and collections.abc.Sequence. Authors of libraries will follow.
I also searched how many 'except IndexError' are in the standard library code. Around 60. That's a rather low number, that can justify adding 'list.get' but not advocate a new syntax.
And roughly 200 of KeyError, another couple hundred of ValueError...
Many KeyErrors can be fixed with a proper use of '.get()', or 'in' operator. Many AttributeErrors can be fixed with use of getattr() or hasattr(), or by designing a better API. Many ValueErrors... wait, there are no ValueError examples in the PEP, so I won't comment.
This is not just about better handling of [missing] default values, but of better exception handling. This PEP adds the ability to use a scalpel instead of a sledge hammer.
I'd say it's another sledge-hammer, but a tad smaller and a cuter one (to some people). But it's still exception handling, still more code than a function call. Yury
Steven D'Aprano wrote:
result = computation( int(arg) except ValueError: abort("Invalid int") )
Actually, not quite so nice as I first thought, since you're relying on the side-effects of abort() rather than returning a value.
Yeah, while I was writing that I wondered whether you should be allowed to write int(arg) except ValueError: raise UserError("Invalid int") That looks heretical, because 'raise' can't in any way be interpreted as a value-returning expression. But you can achieve the same result using a function that always raises and exception, so forbidding it on those grounds would be pointless. And writing it that way at least makes it obvious that it *does* always raise an exception, in the same way that try: i = int(arg) except ValueError: raise UserError("Invalid int") else: result = computation(i) makes it obvious that control can't fall off the end of the except branch. -- Greg
Hi, 2014-02-21 4:15 GMT+01:00 Chris Angelico <rosuav@gmail.com>:
PEP: 463 Title: Exception-catching expressions
Nice PEP. Good luck, it's really hard to modify the language. Be prepared to get many alternatives, criticisms, and suggestions. Good luck to handle them :-) Here is mine. I like the simple case "expr1 except Exception: expr2", but it is used twice like "expr1 except Exception: (expr2 except Exception: expr3)".
* list.pop() - no way to return a default
I never used this once, I didn't know that it exists :)
* min(sequence, default=default) - keyword argument in place of ValueError * sum(sequence, start=default) - slightly different but can do the same job
You should remove the sum() from your list, the start parameter is unrelated. It is used to control input and output types for example. It's not related to an exception.
The proposal adds this::
lst = [1, 2] value = lst[2] except IndexError: "No value"
The PEP looks nice on such simple example...
Consider this example of a two-level cache:: for key in sequence: x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key))) # do something with x
... but I don't like when it is used to build complex expressions. At the first read, I'm unable to understand this long expression. At the second read, I'm still unable to see which instruction will be executed first: lvl1[key] or lvl2[key]? The advantage of the current syntax is that the control flow is obvious, from the top to the bottom: # start try: x = lvl1[key] # first instruction except KeyError: try: x = lvl2[key] except KeyError: x = f(key) # latest instruction # end Thanks to Python indentation, it's easier to see the control flow. After having read a lot of code last 10 years, I now try to follow this rule: one instruction per line. For example, I don't like "if ((res = func()) == NULL)" in the C language, I always split it into two lines. A drawback of writing more than one instruction is that it's hard to debug instruction per instruction (in pdb/gdb). Think for example of "if ((res = func()) == NULL)": if you execute step by step, how do you know which instruction is currently executed? You should maybe write your example differently: x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key))) It looks like the classic try/except syntax.
# sys.abiflags may not be defined on all platforms. _CONFIG_VARS['abiflags'] = sys.abiflags except AttributeError: ''
# Lib/sysconfig.py:529: try: _CONFIG_VARS['abiflags'] = sys.abiflags except AttributeError: # sys.abiflags may not be defined on all platforms. _CONFIG_VARS['abiflags'] = ''
getattr(sys, 'abiflags', '') can be used here.
Retrieve an indexed item, defaulting to None (similar to dict.get):: def getNamedItem(self, name): return self._attrs[name] except KeyError: None
# Lib/xml/dom/minidom.py:573: def getNamedItem(self, name): try: return self._attrs[name] except KeyError: return None
Hum, I don't understand this one. name is an index or a key? If it's a key and self._attrs is a dict, you can use self._attrs.get(name).
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
# Lib/tarfile.py:2198: try: g = grp.getgrnam(tarinfo.gname)[2] except KeyError: g = tarinfo.gid try: u = pwd.getpwnam(tarinfo.uname)[2] except KeyError: u = tarinfo.uid
Note (for myself): the KeyError comes from the function call, not from entry[2].
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")
Oh, again: I prefer the try/except syntax (above), it's more easy to see the control flow and understand the code. Too many instruction per lines, the code is too "dense". It looks like cache[k] is the first executed instruction, but it's wrong: check_permission(k) is executed before. So I prefer this version:
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()
try: (overrides[x] or default).ping() except IndexError: default.ping()
The try/except version should use a variable and call the ping() method outside the try/except block, to ensure that the IndexError comes from overrides[x], not from the ping() method. Victor
On 2/21/2014 5:06 PM, Jan Kaliszewski wrote:
Or even (still being my favorite):
msg = seq[i] except (IndexError: "nothing")
This syntax actually has a benefit: the parenthesized syntax after except could become a list, to allow handling different exceptions from the tried expression with different results: msg = seq[dictionary[i]] except (IndexError: "nothing", KeyError: "serious problems") And still allows nesting: msg = seq[i] except (IndexError: dictionary[i] except (KeyError: "no fallback data for %s" % i))
On Fri, Feb 21, 2014 at 6:06 PM, Jan Kaliszewski <zuo@chopin.edu.pl> wrote:
21.02.2014 18:37, Guido van Rossum wrote:
I'm put off by the ':' syntax myself (it looks to me as if someone forgot a newline somewhere)
As I mentioned at python-ideas I believe that parens neutralize, at least to some extent, that unfortunate statement-ish flavor of the colon.
This one has some statement-like smell:
msg = seq[i] except IndexError: "nothing"
But this looks better, I believe:
msg = (seq[i] except IndexError: "nothing")
Agreed. The same holds true with {'a': 5} and function annotations (e.g. "def f(a: 5)").
Or even (still being my favorite):
msg = seq[i] except (IndexError: "nothing")
+1 -eric
On Fri, Feb 21, 2014 at 6:48 PM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
This new syntax won't magically fix all the code either. But it may let people write code like this:
# I'm sorry, I really can't read this.
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")
You could argue the same thing about people chaining ternary conditional expressions (because it would be about as hard to read). Ditto for lambda. These expression forms (and expressions in general) are useful for brief snippets of code. If someone wants to write expressions that span multiple lines like that, then that's their problem. I don't see how the PEP 463 syntax will make it more likely that someone will abuse expression syntax like that.
or this:
# We happily mask exceptions from getgrnam
g = grp.getgrnam(tarinfo.gname)[2] except KeyError: tarinfo.gid
Either you'd get this anyway: try: g = grp.getgrnam(tarinfo.gname)[2] except KeyError: g = tarinfo.gid or your fallback value will be evaluated prematurely: g = grp.getgrnam(tarinfo.gname).get(2, tarinfo.gid) So to get this right: _info = grp.getgrnam(tarinfo.gname) try: g = _info[2] except KeyError: g = tarinfo.gid vs. _info = grp.getgrnam(tarinfo.gname) g = _info[2] except KeyError: tarinfo.gid
I can't believe you find
list[42] except IndexError: 'spam'
to be better than
list.get(42, 'spam')
What about: list[42] except IndexError: something_expensive() or: list[42] except IndexError: something_lazy_really_we_want_to_put_off() -eric
On Fri, Feb 21, 2014 at 7:07 PM, Victor Stinner <victor.stinner@gmail.com> wrote:
Consider this example of a two-level cache:: for key in sequence: x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key))) # do something with x
... but I don't like when it is used to build complex expressions.
This is true of any expression syntax, not just this proposal--though some expression syntax is more apt to be abused than others: you won't see many multiline int literals! ;)
At the first read, I'm unable to understand this long expression. At the second read, I'm still unable to see which instruction will be executed first: lvl1[key] or lvl2[key]?
The advantage of the current syntax is that the control flow is obvious, from the top to the bottom:
# start try: x = lvl1[key] # first instruction except KeyError: try: x = lvl2[key] except KeyError: x = f(key) # latest instruction # end
Thanks to Python indentation, it's easier to see the control flow.
After having read a lot of code last 10 years, I now try to follow this rule: one instruction per line.
+1
For example, I don't like "if ((res = func()) == NULL)" in the C language, I always split it into two lines. A drawback of writing more than one instruction is that it's hard to debug instruction per instruction (in pdb/gdb). Think for example of "if ((res = func()) == NULL)": if you execute step by step, how do you know which instruction is currently executed?
So true.
You should maybe write your example differently:
x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key)))
It looks like the classic try/except syntax.
+1 -eric
On Sat, Feb 22, 2014 at 6:04 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
* seq[index] - no way to handle a bounds error
We can add 'list.get(index, default)' method, similar to 'Mapping.get'. It's far more easier than introducing new syntax.
That fixes it for the list. Square brackets notation works for any sequence. Are you proposing adding magic to the language so that any class that defines a __getitem__ automatically has a .get() with these semantics? Or, conversely, that defining __getitem__ requires that you also explicitly define get()?
Inconvenience of dict[] raising KeyError was solved by introducing the dict.get() method. And I think that
dct.get('a', 'b')
is 1000 times better than
dct['a'] except KeyError: 'b'
I don't want to see this (or any other syntax) used by anyone.
Every separate method has to be written. That's code that has to be tested, etc. Also, when you go searching for usage of something, you have to cope with the fact that it can be spelled two different ways. This is more noticeable with attributes: print(spam.eggs) # versus print(getattr(spam,"eggs","(no eggs)") The second one definitely doesn't look like it's retrieving spam.eggs, either in plain text search or in an AST walk. I would like to see getattr used primarily where the second argument isn't a literal, and an exception-catching rig used when a default is wanted; that keeps the "x.y" form predictable.
Moreover, I think that explicit handling of IndexError is rather ugly and error prone, using len() is usually reads better.
Retrieve an argument, defaulting to None:: cond = args[1] except IndexError: None
# Lib/pdb.py:803: try: cond = args[1] except IndexError: cond = None
cond = None if (len(args) < 2) else args[1]
There's a distinct difference between those: one is LBYL and the other is EAFP. Maybe it won't matter with retrieving arguments, but it will if you're trying to pop from a queue in a multi-threaded program.
Attempt a translation, falling back on the original:: e.widget = self._nametowidget(W) except KeyError: W
# Lib/tkinter/__init__.py:1222: try: e.widget = self._nametowidget(W) except KeyError: e.widget = W
I'm not sure this is a good example either. I presume '_nametowidget' is some function, that might raise a KeyError because of a bug in its implementation, or to signify that there is no widget 'W'. Your new syntax just helps to work with this error prone api.
I don't know the details; this is exactly what you can see in tkinter at the file and line I point to. Maybe some of these are highlighting other problems to be solved, I don't know, but certainly there will be cases where the API is exactly like this.
Read from an iterator, continuing with blank lines once it's exhausted:: line = readline() except StopIteration: ''
# Lib/lib2to3/pgen2/tokenize.py:370: try: line = readline() except StopIteration: line = ''
Handling StopIteration exception is more common in standard library than IndexError (although not much more), but again, not all of that code is suitable for your syntax. I'd say about 30%, which is about 20-30 spots (correct me if I'm wrong).
I haven't counted them up, but it wouldn't be hard to. Probably not terribly many cases of this in the stdlib, but a reasonable few.
Retrieve platform-specific information (note the DRY improvement); 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: ''
# Lib/sysconfig.py:529: try: _CONFIG_VARS['abiflags'] = sys.abiflags except AttributeError: # sys.abiflags may not be defined on all platforms. _CONFIG_VARS['abiflags'] = ''
Ugly. _CONFIG_VARS['abiflags'] = getattr(sys, 'abiflags', '') Much more readable.
Go ahead and make that change, if you prefer it. That's exactly how it really is currently - the try/except block. Downside is as I mentioned above: it no longer looks like "sys.abiflags", and won't come up when you search for that.
Retrieve an indexed item, defaulting to None (similar to dict.get):: def getNamedItem(self, name): return self._attrs[name] except KeyError: None
# Lib/xml/dom/minidom.py:573: def getNamedItem(self, name): try: return self._attrs[name] except KeyError: return None
_attrs there is a dict (or at least it's something that quaks like a dict, and has [] and keys()), so
return self._attrs.get(name)
To what extent does it have to quack like a dict? In this particular example, I traced through a few levels of "where did _attrs come from", and got bogged down. Does "quacks like a dict" have to include a .get() method?
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
# Lib/tarfile.py:2198: try: g = grp.getgrnam(tarinfo.gname)[2] except KeyError: g = tarinfo.gid try: u = pwd.getpwnam(tarinfo.uname)[2] except KeyError: u = tarinfo.uid
This one is a valid example, but totally unparseable by humans. Moreover, it promotes a bad pattern, as you mask KeyErrors in 'grp.getgrnam(tarinfo.gname)' call.
import pwd pwd.getpwnam("rosuav")
My translation masks nothing that the original didn't mask. The KeyError will come from the function call; it would be IndexError if the function returns a too-short tuple, and that one's allowed to bubble up. pwd.struct_passwd(pw_name='rosuav', pw_passwd='x', pw_uid=1000, pw_gid=1000, pw_gecos='Chris Angelico,,,', pw_dir='/home/rosuav', pw_shell='/bin/bash')
pwd.getpwnam("spam") Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'getpwnam(): name not found: spam'
(Note that it's possible for 'import pwd' to fail, in which case pwd is set to None early in the script. But this particular bit of code checks "if pwd" before continuing anyway, so we don't expect AttributeError here.)
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
I think all of the above more readable with try statement.
Readability is a matter of personal preference, to some extent. I find it clearer to use the shorter form, for the same reason as I'd use this: def query(prompt, default): return input("%s [%s]: "%(prompt, default)) or default I wouldn't use long-hand: def query(prompt, default): s = input("%s [%s]: "%(prompt, default)) if not s: s = default return s It's easier to see that it's calling something, and defaulting to something else.
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", _)
If you replace '_' with a 'msg' (why did you use '_'??) then try statements are *much* more readable.
I've removed that example. The reason for using _ was because I wanted it to have the "feel" of still being an expression, where nothing's named. But it's not a very helpful example anyway; part of the confusion comes from the if/else in the middle, which completely wrecks evaluation order expectations.
Lib/ipaddress.py:343:: try: ips.append(ip.ip) except AttributeError: ips.append(ip.network_address) Becomes:: ips.append(ip.ip except AttributeError: ip.network_address)
or it may become:
ips.append(getattr(ip, 'ip', ip.network_address))
or
address = getattr(ip, 'ip', ip.network_address) ips.append(address)
There's a subtle difference here that makes that not equivalent. With the original try/except statement, evaluation proceeds thus: 1) Attempt to look up ip.ip. If that succeeds, call ips.append(). 2) If AttributeError is not thrown in #1, done. Otherwise, proceed. 3) Attempt to look up ip.network_address. If that succeeds, call ips.append. 4) Any exception raised will propagate. This means that, if neither ip nor network_address is defined, an AttributeError will come up, but that if ip is, network_address won't even be looked at. My version narrows the scope slightly, but is functionally similar. 1) Attempt to look up ip.ip. 2) If AttributeError is thrown in #1, attempt to look up ip.network_address. 3) If either #1 or #2 succeeds, call ips.append. 4) Any exception raised anywhere else will propagate. Your version, however, inverts the evaluation order: 1) Attempt to look up ip.network_address 2) If AttributeError is thrown in #1, propagate it up and stop evaluating. 3) Retrieve ip.ip, defaulting to the looked-up network address. 4) Pass that to ips.append(). It's probably not safe to use 'or' here, but if you can be sure ip.ip will never be blank, you could get lazy evaluation this way: ips.append(getattr(ip, 'ip', '') or ip.network_address) But at this point, the clarity advantage over the except clause is diminishing, plus it conflates AttributeError and ''.
Yes, some examples look neat. But your syntax is much easier to abuse, than 'if..else' expression, and if people start abusing it, Python will simply loose it's readability advantage.
If people start abusing it, style guides can tell them off. Unlike the if/else operator, evaluation of "expr except Exception: default" happens in strict left-to-right order, so in that sense it's _less_ confusing. I'll be adding a paragraph to the PEP shortly explaining that. ChrisA
On Sat, Feb 22, 2014 at 9:06 AM, Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
Nick Coghlan wrote:
On 21 February 2014 13:15, Chris Angelico <rosuav@gmail.com> wrote:
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
I'd like to make the case that the PEP should adopt this as its default position.
I generally agree, but I'd like to point out that this doesn't necessarily mean making the parenthesizing rules as strict as they are for generator expressions.
The starting point for genexps is that the parens are part of the syntax, the same way that square brackets are part of the syntax of a list comprehension; we only allow them to be omitted in very special circumstances.
On the other hand, I don't think there's any harm in allowing an except expression to stand on its own when there is no risk of ambiguity, e.g.
foo = things[i] except IndexError: None
should be allowed, just as we allow
x = a if b else c
and don't require
x = (a if b else c)
I'm inclined to agree with you. I fought against the mandated parens for a long time. Would be happy to un-mandate them, as long as there's no way for there to be ambiguity. Is CPython able to make an operator non-associative? That is, to allow these: value = expr except Exception: default value = (expr except Exception: default) except Exception: default value = expr except Exception: (default except Exception: default) but not this: value = expr except Exception: default except Exception: default ? By effectively demanding parens any time two except-expressions meet, we could leave the option open to read multiple clauses off a single base exception. If that can be done, and if people's need for clarity can be satisfied, and if the rules aren't too complicated, I'd be happy to go back to "parens are optional". ChrisA
On Sat, Feb 22, 2014 at 10:29 AM, Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
Antoine Pitrou wrote:
lst = [1, 2] value = lst[2] except IndexError: "No value"
the gain in concision is counterbalanced by a loss in readability,
This version might be more readable:
value = lst[2] except "No value" if IndexError
since it puts the normal and exceptional values next to each other, and relegates the exception type (which is of much less interest) to a trailing aside.
I'll expand on this in the PEP shortly, but there are two downsides to this. 1) Everywhere else in Python, "if" is followed by a boolean, and "except" is followed by an exception list. As a boolean, IndexError is always going to be true, which will confuse a human; and "No value" doesn't look like a modern exception at all. 2) Order of evaluation puts the main expression first, then the exception list, and lastly the default. Putting them in another order in the code is confusing. With ternary if/else, this is justified, but it's usually something to avoid. 3) Erm, among the downsides are such diverse elements... ahem. The part after the except clause is a full expression, and could plausibly have the if/else ternary operator. Starting with some expression, then if, then another expression, means you have to look for an else before you can be sure you're reading it correctly. Of course, any sane programmer who actually puts an if/else inside an except/if should parenthesize, but when you read someone else's code, you have to be prepared for the more normal sort of programmer. ChrisA
On Sat, Feb 22, 2014 at 12:10 PM, Glenn Linderman <v+python@g.nevcal.com> wrote:
Here's a challenge: There has been a big thread about None versus (SQL) Null. Show how an except: expression can help the DB API more easily convert from using None to using a new Null singleton, and you'll have a winner :)
Heh! I'm not entirely sure how that would work, as I've only skimmed the DB API thread, but I understand part of it is to do with sorting. I'm not sure how you could embed an except-expression into that without either changing the sort function or potentially doing part of the sort twice: lst = sorted(stuff) except TypeError: sorted(stuff, key=keyfunc) at which point you may as well go straight to the key= form from the start. ChrisA
On Sat, Feb 22, 2014 at 12:55 PM, Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
Steven D'Aprano wrote:
result = computation( int(arg) except ValueError: abort("Invalid int") )
Actually, not quite so nice as I first thought, since you're relying on the side-effects of abort() rather than returning a value.
Yeah, while I was writing that I wondered whether you should be allowed to write
int(arg) except ValueError: raise UserError("Invalid int")
That looks heretical, because 'raise' can't in any way be interpreted as a value-returning expression. But you can achieve the same result using a function that always raises and exception, so forbidding it on those grounds would be pointless.
def throw(exc): raise exc int(arg) except ValueError: throw(UserError("Invalid int")) Tiny helper function and then it doesn't need any special syntax. It's not so much that PEP 463 forbids this, as that it doesn't explicitly permit it. The 'default' part of the syntax is an expression, raise is not an expression, ergo it's not permitted. But if you think this is sufficiently common (translate an exception on the way through), show some code and justify its addition - or just write up a separate proposal for "raise X" to become an expression, same as "yield X" is. ChrisA
On Sat, Feb 22, 2014 at 1:07 PM, Victor Stinner <victor.stinner@gmail.com> wrote:
At the first read, I'm unable to understand this long expression. At the second read, I'm still unable to see which instruction will be executed first: lvl1[key] or lvl2[key]?
The advantage of the current syntax is that the control flow is obvious, from the top to the bottom:
# start try: x = lvl1[key] # first instruction except KeyError: try: x = lvl2[key] except KeyError: x = f(key) # latest instruction # end
That's why I'm strongly in favour of syntax variants that have evaluation order be equally obvious: left to right. Its notation may be uglier, but C's ternary operator does get this right, where Python's executes from the inside out. It's not a big deal when most of it is constants, but it can help a lot when the expressions nest. ChrisA
On Sat, Feb 22, 2014 at 3:04 PM, Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Feb 22, 2014 at 1:07 PM, Victor Stinner <victor.stinner@gmail.com> wrote:
At the first read, I'm unable to understand this long expression. At the second read, I'm still unable to see which instruction will be executed first: lvl1[key] or lvl2[key]?
The advantage of the current syntax is that the control flow is obvious, from the top to the bottom:
# start try: x = lvl1[key] # first instruction except KeyError: try: x = lvl2[key] except KeyError: x = f(key) # latest instruction # end
That's why I'm strongly in favour of syntax variants that have evaluation order be equally obvious: left to right. Its notation may be uglier, but C's ternary operator does get this right, where Python's executes from the inside out. It's not a big deal when most of it is constants, but it can help a lot when the expressions nest.
I've added a couple of paragraphs to my draft PEP: https://raw.github.com/Rosuav/ExceptExpr/master/pep-0463.txt If someone could please commit that version to the official repo? Or I can submit a diff against the peps repo if that would be easier. ChrisA
On 2/21/2014, 10:42 PM, Chris Angelico wrote:
On Sat, Feb 22, 2014 at 6:04 AM, Yury Selivanov <yselivanov.ml@gmail.com> wrote:
* seq[index] - no way to handle a bounds error We can add 'list.get(index, default)' method, similar to 'Mapping.get'. It's far more easier than introducing new syntax. That fixes it for the list. Square brackets notation works for any sequence. Are you proposing adding magic to the language so that any class that defines a __getitem__ automatically has a .get() with these semantics? Or, conversely, that defining __getitem__ requires that you also explicitly define get()?
Inconvenience of dict[] raising KeyError was solved by introducing the dict.get() method. And I think that
dct.get('a', 'b')
is 1000 times better than
dct['a'] except KeyError: 'b'
I don't want to see this (or any other syntax) used by anyone. Every separate method has to be written. That's code that has to be tested, etc. Also, when you go searching for usage of something, you have to cope with the fact that it can be spelled two different ways. This is more noticeable with attributes:
print(spam.eggs) # versus print(getattr(spam,"eggs","(no eggs)")
The second one definitely doesn't look like it's retrieving spam.eggs, either in plain text search or in an AST walk. And that's not a common thing to use AttributeError/getattr at all. It is common in frameworks, yes, but not that much in client code. I would like to see getattr used primarily where the second argument isn't a literal, and an exception-catching rig used when a default is wanted; that keeps the "x.y" form predictable. I understand. But I still think that using getattr is better
Moreover, I think that explicit handling of IndexError is rather ugly and error prone, using len() is usually reads better.
Retrieve an argument, defaulting to None:: cond = args[1] except IndexError: None
# Lib/pdb.py:803: try: cond = args[1] except IndexError: cond = None
cond = None if (len(args) < 2) else args[1] There's a distinct difference between those: one is LBYL and the other is EAFP. Maybe it won't matter with retrieving arguments, but it will if you're trying to pop from a queue in a multi-threaded program. Using a method that modifies its underlying object deep in expression is also questionable, in terms of readability and
No, I proposed to consider adding 'list.get()' and 'collections.abc.Sequence.get()' (or a better name), if catching IndexError is such a popular pattern. We can also fix stdlib. Authors of the python libraries/ frameworks will likely follow. No magic in the language, no requirements on __getitem__, obviously. than 'a.b except AttributeError: c' maintainability of the code. I find try..except statement more favourable here.
Attempt a translation, falling back on the original:: e.widget = self._nametowidget(W) except KeyError: W
# Lib/tkinter/__init__.py:1222: try: e.widget = self._nametowidget(W) except KeyError: e.widget = W I'm not sure this is a good example either. I presume '_nametowidget' is some function, that might raise a KeyError because of a bug in its implementation, or to signify that there is no widget 'W'. Your new syntax just helps to work with this error prone api. I don't know the details; this is exactly what you can see in tkinter at the file and line I point to. Maybe some of these are highlighting other problems to be solved, I don't know, but certainly there will be cases where the API is exactly like this.
Read from an iterator, continuing with blank lines once it's exhausted:: line = readline() except StopIteration: ''
# Lib/lib2to3/pgen2/tokenize.py:370: try: line = readline() except StopIteration: line = '' Handling StopIteration exception is more common in standard library than IndexError (although not much more), but again, not all of that code is suitable for your syntax. I'd say about 30%, which is about 20-30 spots (correct me if I'm wrong). I haven't counted them up, but it wouldn't be hard to. Probably not terribly many cases of this in the stdlib, but a reasonable few.
Just having a "reasonable few" use cases in huge stdlib isn't a warrant for new syntax.
Retrieve platform-specific information (note the DRY improvement); 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: ''
# Lib/sysconfig.py:529: try: _CONFIG_VARS['abiflags'] = sys.abiflags except AttributeError: # sys.abiflags may not be defined on all platforms. _CONFIG_VARS['abiflags'] = '' Ugly. _CONFIG_VARS['abiflags'] = getattr(sys, 'abiflags', '') Much more readable. Go ahead and make that change, if you prefer it. That's exactly how it really is currently - the try/except block. Downside is as I mentioned above: it no longer looks like "sys.abiflags", and won't come up when you search for that.
Retrieve an indexed item, defaulting to None (similar to dict.get):: def getNamedItem(self, name): return self._attrs[name] except KeyError: None
# Lib/xml/dom/minidom.py:573: def getNamedItem(self, name): try: return self._attrs[name] except KeyError: return None _attrs there is a dict (or at least it's something that quaks like a dict, and has [] and keys()), so
return self._attrs.get(name) To what extent does it have to quack like a dict? In this particular example, I traced through a few levels of "where did _attrs come from", and got bogged down. Does "quacks like a dict" have to include a .get() method? The point is that whoever wrote that code knew what is _attrs. Likely it's a dict, since they use '.keys()' on it. And likely,
I'd search for 'abiflags' since it's not a common name ;) But again, I get your point here. that person just prefers try..except instead of using '.get()'. And I just want to say, that this particular example isn't a good use case for the new syntax you propose.
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
# Lib/tarfile.py:2198: try: g = grp.getgrnam(tarinfo.gname)[2] except KeyError: g = tarinfo.gid try: u = pwd.getpwnam(tarinfo.uname)[2] except KeyError: u = tarinfo.uid This one is a valid example, but totally unparseable by humans. Moreover, it promotes a bad pattern, as you mask KeyErrors in 'grp.getgrnam(tarinfo.gname)' call. My translation masks nothing that the original didn't mask. The KeyError will come from the function call; it would be IndexError if the function returns a too-short tuple, and that one's allowed to bubble up. Right
import pwd pwd.getpwnam("rosuav") pwd.struct_passwd(pw_name='rosuav', pw_passwd='x', pw_uid=1000, pw_gid=1000, pw_gecos='Chris Angelico,,,', pw_dir='/home/rosuav', pw_shell='/bin/bash') pwd.getpwnam("spam") Traceback (most recent call last): File "<stdin>", line 1, in <module> KeyError: 'getpwnam(): name not found: spam'
(Note that it's possible for 'import pwd' to fail, in which case pwd is set to None early in the script. But this particular bit of code checks "if pwd" before continuing anyway, so we don't expect AttributeError here.)
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 I think all of the above more readable with try statement. Readability is a matter of personal preference, to some extent. I find it clearer to use the shorter form, for the same reason as I'd use this:
def query(prompt, default): return input("%s [%s]: "%(prompt, default)) or default
I wouldn't use long-hand:
def query(prompt, default): s = input("%s [%s]: "%(prompt, default)) if not s: s = default return s
It's easier to see that it's calling something, and defaulting to something else.
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", _)
If you replace '_' with a 'msg' (why did you use '_'??) then try statements are *much* more readable. I've removed that example. The reason for using _ was because I wanted it to have the "feel" of still being an expression, where nothing's named. But it's not a very helpful example anyway; part of the confusion comes from the if/else in the middle, which completely wrecks evaluation order expectations.
I think that this example is better to be kept ;) But up to you. While each PEP want's to be Final, it's still good to see how people can abuse it. And this example is excellent in that regard.
Lib/ipaddress.py:343:: try: ips.append(ip.ip) except AttributeError: ips.append(ip.network_address) Becomes:: ips.append(ip.ip except AttributeError: ip.network_address) or it may become:
ips.append(getattr(ip, 'ip', ip.network_address))
or
address = getattr(ip, 'ip', ip.network_address) ips.append(address) There's a subtle difference here that makes that not equivalent. With the original try/except statement, evaluation proceeds thus:
1) Attempt to look up ip.ip. If that succeeds, call ips.append(). 2) If AttributeError is not thrown in #1, done. Otherwise, proceed. 3) Attempt to look up ip.network_address. If that succeeds, call ips.append. 4) Any exception raised will propagate.
This means that, if neither ip nor network_address is defined, an AttributeError will come up, but that if ip is, network_address won't even be looked at.
My version narrows the scope slightly, but is functionally similar.
1) Attempt to look up ip.ip. 2) If AttributeError is thrown in #1, attempt to look up ip.network_address. 3) If either #1 or #2 succeeds, call ips.append. 4) Any exception raised anywhere else will propagate.
Your version, however, inverts the evaluation order:
1) Attempt to look up ip.network_address 2) If AttributeError is thrown in #1, propagate it up and stop evaluating. 3) Retrieve ip.ip, defaulting to the looked-up network address. 4) Pass that to ips.append().
It's probably not safe to use 'or' here, but if you can be sure ip.ip will never be blank, you could get lazy evaluation this way:
ips.append(getattr(ip, 'ip', '') or ip.network_address)
But at this point, the clarity advantage over the except clause is diminishing, plus it conflates AttributeError and ''.
Yes, conditional evaluation a good part of the new syntax (but again, this *particular* example doesn't show it; better to use some ORM maybe, where each __getattr__ is potentially a query).
Yes, some examples look neat. But your syntax is much easier to abuse, than 'if..else' expression, and if people start abusing it, Python will simply loose it's readability advantage. If people start abusing it, style guides can tell them off. Unlike the if/else operator, evaluation of "expr except Exception: default" happens in strict left-to-right order, so in that sense it's _less_ confusing. I'll be adding a paragraph to the PEP shortly explaining that.
There is one more thing: "There should be one-- and preferably only one --obvious way to do it." -- so far it's true for all python language constructs. They all are solving a real pain point. Maybe it's just me, but I fail to see the real pain point this proposal solves. To conclude: 1. I'm still not convinced that the new syntax is really necessary. 2. You should probably invest some time in finding better examples for the PEP. Examples, that will focus on its strengths, and show the real need. 3. Syntax. Using ':' in it makes it look bad. I know there are many other alternatives, and 400+ emails on python-ideas, but still, we couldn't find a viable alternative. Perhaps, we need to look for it some more time. My 2 cents. Yury
On 2/21/2014, 11:22 PM, Chris Angelico wrote:
I've added a couple of paragraphs to my draft PEP:
https://raw.github.com/Rosuav/ExceptExpr/master/pep-0463.txt
If someone could please commit that version to the official repo? Or I can submit a diff against the peps repo if that would be easier. I've committed the new version.
Yury
On Fri, Feb 21, 2014 at 02:04:45PM -0500, Yury Selivanov wrote:
Inconvenience of dict[] raising KeyError was solved by introducing the dict.get() method. And I think that
dct.get('a', 'b')
is 1000 times better than
dct['a'] except KeyError: 'b'
I don't think it is better. I think that if we had good exception-catching syntax, we wouldn't need a get method. "get" methods do not solve the problem, they are shift it. Every class that defines [] look-ups also needs a get method. We can write dict.get(key, default), but we can't write list.get(index, default). If we "fix" list, we then find that tuple.get(index, default) fails. So we "fix" tuple, and then discover that str.get(index, default) fails. If Python were Ruby, then all sequences could inherit from one Sequence class that defines: def get(self, index, default=None): try: return self[index] except IndexError: return default and similarly for mappings, with KeyError instead of IndexError. But this is Python, not Ruby, and there is no requirement that sequences must inherit from the same parent class. They just have to provide the same duck-typed interface. Having added a get method to every mapping and sequence, you then call list.pop() and discover that it too needs a default. And so on, forever. We're forever chasing the next method and function, adding default parameters to everything in sight. The latest example is max and min. Then there are functions or methods which come in pairs, like str.find and str.index. From time to time people ask for a version of list.index that returns a default value instead of raising. Should we give them one? I don't think Python is a better language by adding complexity into the libraries and built-ins in this way. (And let's not forget that while methods and functions can have default parameters, expressions cannot. So there is a whole class of potential operations that can never be given a default, because they aren't a function call.) The essence of programming is combining primitive constructs to make more complicated ones, not in the proliferation of special-purpose methods or functions. "get" is less primitive than normal [] lookup, and it exists for a special purpose: to avoid needing to break apart what ought is a single conceptual expression into a multi-line statement. Consider a single conceptual chunk of code, the expression process(mydict[key]). In the past, if you wanted to use a default value if the key didn't exist, the only alternative was to break that expression up into two statements, regardless of whether you used LBYL or EAFP idioms: if key in mydict: tmp = mydict[key] else: tmp = default result = process(tmp) try: tmp = mydict[key] except KeyError: tmp = default result = process(tmp) Consequently, to express a single expression *as a single expression*, we needed an expression that can do the same thing, and in the past that had to be a method: result = process(mydict.get(key, default)) But now we have a LBYL expression form: result = process(mydict[key] if key in mydict else default) and are suggesting a EAFP form: result = process(mydict[key] except KeyError: default) If Python had this syntax 20 years ago, would dicts have grown a get method? I doubt it very much. People would be told to wrap it in an except expression.
Consider this example of a two-level cache:: for key in sequence: x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key))) # do something with x
I'm sorry, it took me a minute to understand what your example is doing. I would rather see two try..except blocks than this.
It helps to break it up more appropriately. (Also less cryptic names.) I can see PEP 8 will need some changes if this syntax is approved. for key in sequence: x = (cache1[key] except KeyError: (cache2[key] except KeyError: f(key))) # Do something with x This proposal is not about saving lines of code, and if examples are written jammed into a single line or two, and that hurts readability, they should be formatted better. It is about expressing the EAFP idiom in an expression, without giving up the useful property that the "except" clause is only evaluated when needed, not in advance. (Chris, I think that ought to go in the motivation section of the PEP.) -- Steven
On Sat, Feb 22, 2014 at 4:01 PM, Steven D'Aprano <steve@pearwood.info> wrote:
(Chris, I think that ought to go in the motivation section of the PEP.)
Added to my draft, and here's the peps diff: diff -r c52a2ae3d98e pep-0463.txt --- a/pep-0463.txt Fri Feb 21 23:27:51 2014 -0500 +++ b/pep-0463.txt Sat Feb 22 16:33:37 2014 +1100 @@ -43,6 +43,34 @@ * statistics.mean(data) - no way to handle an empty iterator +Had this facility existed early in Python's history, there would have been +no need to create dict.get() and related methods; the one obvious way to +handle an absent key would be to respond to the exception. One method is +written which signal the absence in one way, and one consistent technique +is used to respond to the absence. Instead, we have dict.get(), and as of +Python 3.4, we also have min(... default=default), and myriad others. We +have a LBYL syntax for testing inside an expression, but there is currently +no EAFP notation; compare the following:: + + # LBYL: + if key in dic: + process(dic[key]) + else: + process(None) + # As an expression: + process(dic[key] if key in dic else None) + + # EAFP: + try: + process(dic[key]) + except KeyError: + process(None) + # As an expression: + process(dic[key] except KeyError: None) + +Python generally recommends the EAFP policy, but must then proliferate +utility functions like dic.get(key,None) to enable this. + Rationale =========
On 2/21/2014 7:57 PM, Chris Angelico wrote:
Here's a challenge: There has been a big thread about None versus (SQL) Null. Show how an except: expression can help the DB API more easily convert from using None to using a new Null singleton, and you'll have a winner :) Heh! I'm not entirely sure how that would work, as I've only skimmed
On Sat, Feb 22, 2014 at 12:10 PM, Glenn Linderman <v+python@g.nevcal.com> wrote: the DB API thread, but I understand part of it is to do with sorting. I'm not sure how you could embed an except-expression into that without either changing the sort function or potentially doing part of the sort twice:
lst = sorted(stuff) except TypeError: sorted(stuff, key=keyfunc)
at which point you may as well go straight to the key= form from the start.
Yes, the "challenge" was sort of tongue-in-cheek... it was the latest heavily opinionated thread with no conclusion... and if a particular feature could help arrive at a good solution I think a lot of people would be happy! And probably a lot would still think it wasn't a good solution! It does help acceptance of a new feature to describe and demonstrate a solution to a compelling problem; Stephen's recent explanation of LBYL and EAFP expressions helped make it more compelling to me, it sounded like you added some of that to the PEP, which is good. Some folks didn't find the PEP examples compelling, it seems. The sorting is a smoke-screen. The real problem is that None and Null are not the same thing, and so internally the DB Api should invent and use Null, and translate to None (optionally) at the interfaces, for backwards compatibility until their users can update their software to use Null also. The implementation of Null can handle the comparison/sorting issue... there are a fixed number of types for SQL, so it isn't that hard.
Ethan Furman writes:
On 02/21/2014 07:46 PM, Chris Angelico wrote:
but not this:
value = expr except Exception: default except Exception: default
This should be the way it works. Nothing is gained in readability by turning a try with multiple except statements into an expression.
Examples have been given several times. In general, if 'expr' is a function call, it may well have a couple of different ways to fail which imply different default values. interpolable = func(key) except TypeError: "not a string: %s" % key \ except KeyError: "no such key: %s" % key print("Some message that refers to '%s' % interpolable") versus try: interpolable = func(key) except TypeError: interpolable = "not a string: %s" % key except KeyError: interpolable = "no such key: %s" % key print("Some message that refers to '%s' % interpolable") I think the latter begs to be written as the former.
Steven D'Aprano writes:
On Fri, Feb 21, 2014 at 02:04:45PM -0500, Yury Selivanov wrote:
Inconvenience of dict[] raising KeyError was solved by introducing the dict.get() method. And I think that
dct.get('a', 'b')
is 1000 times better than
dct['a'] except KeyError: 'b'
Aside from the "no need for 'get'" argument, I like the looks of the latter better, because dct.get('a', 'b') could be doing anything, while the syntax in the expression is defined by the language and says exactly what it's doing, even if read in English (obviously you have to be a Python programmer to understand that, but you don't need to be Dutch).
I don't think it is better. I think that if we had good exception-catching syntax, we wouldn't need a get method.
"get" methods do not solve the problem, they are shift it.
Note in support: I originally thought that "get" methods would be more efficient, but since Nick pointed out that "haveattr" is implemented by catching the exception (Yikes! LBYL implemented by using EAFP!), I assume that get methods also are (explicitly or implicitly) implemented that way. I'm not a huge fan of the proposal (in my own code there aren't a lot of potential uses, and I think the colon syntax is a little ugly), but the "eliminates need for .get()" argument persuades me that the colon syntax is better than nothing.
On 02/21/2014 10:57 PM, Stephen J. Turnbull wrote:
Ethan Furman writes:
On 02/21/2014 07:46 PM, Chris Angelico wrote:
but not this:
value = expr except Exception: default except Exception: default
This should be the way it works. Nothing is gained in readability by turning a try with multiple except statements into an expression.
Examples have been given several times. In general, if 'expr' is a function call, it may well have a couple of different ways to fail which imply different default values.
interpolable = func(key) except TypeError: "not a string: %s" % key \ except KeyError: "no such key: %s" % key print("Some message that refers to '%s' % interpolable")
versus
try: interpolable = func(key) except TypeError: interpolable = "not a string: %s" % key except KeyError: interpolable = "no such key: %s" % key print("Some message that refers to '%s' % interpolable")
I think the latter begs to be written as the former.
Okay, that's the best example of that style I've seen so far (sorry, Chris, if something similar was in the PEP and I missed it). I will yield the point that something is gained -- still, I think it is a small something compared to converting a nested except statement into an expression, and if only allowing one or the other makes the whole thing simpler I vote for the nested excepts to be converted, not the already easily read multiple excepts. -- ~Ethan~
On Sat, 22 Feb 2014 15:57:02 +0900 "Stephen J. Turnbull" <stephen@xemacs.org> wrote:
try: interpolable = func(key) except TypeError: interpolable = "not a string: %s" % key except KeyError: interpolable = "no such key: %s" % key print("Some message that refers to '%s' % interpolable")
I think that's a rare enough case, though (compared to the other idioms showcased in the PEP), that we needn't care about it. Regards Antoine.
On Fri, 21 Feb 2014 09:37:29 -0800 Guido van Rossum <guido@python.org> wrote:
I'm put off by the ':' syntax myself (it looks to me as if someone forgot a newline somewhere) but 'then' feels even weirder (it's been hard-coded in my brain as meaning the first branch of an 'if').
Would 'else' work rather than 'then'? Regards Antoine.
On Sat, 22 Feb 2014 16:12:27 +0900 "Stephen J. Turnbull" <stephen@xemacs.org> wrote:
Note in support: I originally thought that "get" methods would be more efficient, but since Nick pointed out that "haveattr" is implemented by catching the exception (Yikes! LBYL implemented by using EAFP!), I assume that get methods also are (explicitly or implicitly) implemented that way.
Well, the only way to know that a key (or attribute) exists is to do the lookup. What else would you suggest? And, yes, EAFP can avoid race conditions and the like (besides being more efficient with non-trivial keys). Regards Antoine.
On Fri, 21 Feb 2014 19:49:20 -0700 Eric Snow <ericsnowcurrently@gmail.com> wrote:
On Fri, Feb 21, 2014 at 7:07 PM, Victor Stinner <victor.stinner@gmail.com> wrote:
Consider this example of a two-level cache:: for key in sequence: x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key))) # do something with x
... but I don't like when it is used to build complex expressions.
This is true of any expression syntax, not just this proposal--though some expression syntax is more apt to be abused than others: you won't see many multiline int literals! ;)
But this is precisely why Python has refrained from making everything an expression. For example, why assignements are not expressions. Regards Antoine.
On Sat, Feb 22, 2014 at 8:20 PM, Antoine Pitrou <solipsis@pitrou.net> wrote:
On Sat, 22 Feb 2014 16:12:27 +0900 "Stephen J. Turnbull" <stephen@xemacs.org> wrote:
Note in support: I originally thought that "get" methods would be more efficient, but since Nick pointed out that "haveattr" is implemented by catching the exception (Yikes! LBYL implemented by using EAFP!), I assume that get methods also are (explicitly or implicitly) implemented that way.
Well, the only way to know that a key (or attribute) exists is to do the lookup. What else would you suggest?
And, yes, EAFP can avoid race conditions and the like (besides being more efficient with non-trivial keys).
Which means that, fundamentally, EAFP is the way to do it. So if PEP 463 expressions had existed from the beginning, hasattr() probably wouldn't have been written - people would just use an except-expression instead. ChrisA
On Sat, 22 Feb 2014 20:29:27 +1100 Chris Angelico <rosuav@gmail.com> wrote:
Which means that, fundamentally, EAFP is the way to do it. So if PEP 463 expressions had existed from the beginning, hasattr() probably wouldn't have been written - people would just use an except-expression instead.
Really? hasattr() is much easier to write than the corresponding except-expression. Regards Antoine.
On Fri, Feb 21, 2014 at 7:46 PM, Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Feb 22, 2014 at 9:06 AM, Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
Nick Coghlan wrote:
On 21 February 2014 13:15, Chris Angelico <rosuav@gmail.com> wrote:
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
I'd like to make the case that the PEP should adopt this as its default position.
I generally agree, but I'd like to point out that this doesn't necessarily mean making the parenthesizing rules as strict as they are for generator expressions.
The starting point for genexps is that the parens are part of the syntax, the same way that square brackets are part of the syntax of a list comprehension; we only allow them to be omitted in very special circumstances.
On the other hand, I don't think there's any harm in allowing an except expression to stand on its own when there is no risk of ambiguity, e.g.
foo = things[i] except IndexError: None
should be allowed, just as we allow
x = a if b else c
and don't require
x = (a if b else c)
I'm inclined to agree with you. I fought against the mandated parens for a long time. Would be happy to un-mandate them, as long as there's no way for there to be ambiguity. Is CPython able to make an operator non-associative? That is, to allow these:
value = expr except Exception: default value = (expr except Exception: default) except Exception: default value = expr except Exception: (default except Exception: default)
but not this:
value = expr except Exception: default except Exception: default
?
Yes, although how to do it depends on what else 'default' and 'expr' can be. The simple answer is to make 'expr' and 'default' be subsets of expressions that don't include the 'except' expression themselves. Just like how in 'A if C else B', 'A' and 'C' do not include the 'if' syntax, but 'B' does: you need parentheses in '(a if c1 else b) if c2 else d' and 'a if (c1 if c3 else c2) else b' but not in 'a if c1 else b if c2 else d'. However, the question becomes what to do with 'lambda' and 'if-else'. Which of these should be valid (and mean what it means with the parentheses, which is how the PEP currently suggests it should be; ones without parenthesized alternatives are unambiguous, as far as I can tell.) 1. make_adder(x) except TypeError: lambda y: y + 0 2. lambda x: x + 0 except TypeError: 1 -> lambda x: (x + 0 except TypeError: 1) 3. A if f(A) except TypeError: C else B 4. f(A1) except TypeError: A2 if C else B -> f(A1) except TypeError: (A2 if C else B) 5. f(A1) except TypeError if C else ValueError: f(A2) 6. A if C else f(B) except TypeError: B -> (A if C else f(B)) except TypeError: B 7. f(A) except E1(A) except E2(A): E2: E1 -> f(A) except (E1(A) except E2(A): E2): E1 8. f(A) or f(B) except TypeError: f(C) -> (f(A) or f(B)) except TypeError: f(C) 9. f(A) except TypeError: f(B) or f(C) -> f(A) except TypeError: (f(B) or f(C)) 10. A == f(A) except TypeError: B -> (A == f(A)) except TypeError: B 11. f(A) except TypeError: A == B -> f(A) except TypeError: (A == B) 12. A + B except TypeError: C -> (A + B) except TypeError: C 13. f(A) except TypeError: A + B -> f(A) except TypeError: (A + B) #6 in particular and to some extent #4, #8 and #10 look wrong to me, so if they'd be allowed without parentheses perhaps the precedence of if-expr and except should be more nuanced. ('between lambda and ifexpr' is not really a sensible way to describe precedence anyway, since despite the insistence of the reference manual, the precedence of 'lambda' and 'ifexpr' is at best 'mixed': the grammar actually tries to match ifexpr first, but it works the way we all expect because 'lambda' is not an infix operator.) It's easy to disallow most of these (allowing only their parenthesized versions), the question is where to draw the line :) Some of these require a bit more work in the grammar, duplicating some of the grammar nodes, but it shouldn't be too bad. However, disallowing any infix operators in the 'default' expression means it'll still be valid, just mean something different; #9, for example, would be parsed as '(f(A) except TypeError: f(B)) or f(C)' if the 'default' expression couldn't be an or-expression. Disallowing it in the 'expr' expression wouldn't have that effect without parentheses. (FWIW, I have a working patch without tests that allows all of these, I'll upload it tonight so people can play with it. Oh, and FWIW, currently I'm +0 on the idea, -0 on the specific syntax.) -- Thomas Wouters <thomas@python.org> Hi! I'm an email virus! Think twice before sending your email to help me spread!
On Sat, Feb 22, 2014 at 8:58 PM, Antoine Pitrou <solipsis@pitrou.net> wrote:
On Sat, 22 Feb 2014 20:29:27 +1100 Chris Angelico <rosuav@gmail.com> wrote:
Which means that, fundamentally, EAFP is the way to do it. So if PEP 463 expressions had existed from the beginning, hasattr() probably wouldn't have been written - people would just use an except-expression instead.
Really? hasattr() is much easier to write than the corresponding except-expression.
But would it be sufficiently easier to justify the creation of a built-in? Imagine this were the other way around: we have except-expressions, but we don't have hasattr. Now someone comes onto python-ideas and says, "Wouldn't it be nice if we had a hasattr() function to tell us whether something has an attribute or not". Considering that hasattr can be easily implemented using an except-expression, it would be unlikely to be considered worthy of a built-in. ChrisA
On Sat, 22 Feb 2014 21:09:07 +1100 Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Feb 22, 2014 at 8:58 PM, Antoine Pitrou <solipsis@pitrou.net> wrote:
On Sat, 22 Feb 2014 20:29:27 +1100 Chris Angelico <rosuav@gmail.com> wrote:
Which means that, fundamentally, EAFP is the way to do it. So if PEP 463 expressions had existed from the beginning, hasattr() probably wouldn't have been written - people would just use an except-expression instead.
Really? hasattr() is much easier to write than the corresponding except-expression.
But would it be sufficiently easier to justify the creation of a built-in?
Well, can you propose the corresponding except-expression? Regards Antoine.
Antoine Pitrou writes:
Well, the only way to know that a key (or attribute) exists is to do the lookup. What else would you suggest?
Do the lookup at the C level (or whatever the implementation language is) and generate no exception, of course. That's what would make it possibly more efficient.
On sam., 2014-02-22 at 19:29 +0900, Stephen J. Turnbull wrote:
Antoine Pitrou writes:
Well, the only way to know that a key (or attribute) exists is to do the lookup. What else would you suggest?
Do the lookup at the C level (or whatever the implementation language is) and generate no exception, of course. That's what would make it possibly more efficient.
Let's see: - hasattr() does the lookup at the C level, and silences the AttributeError - dict.get() does the lookup at the C level, and doesn't generate an exception So apart from the minor inefficiency of generating and silencing the AttributeError, those functions already do what you suggest. Regards Antoine.
On Sat, Feb 22, 2014 at 9:17 PM, Antoine Pitrou <solipsis@pitrou.net> wrote:
On Sat, 22 Feb 2014 21:09:07 +1100 Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Feb 22, 2014 at 8:58 PM, Antoine Pitrou <solipsis@pitrou.net> wrote:
On Sat, 22 Feb 2014 20:29:27 +1100 Chris Angelico <rosuav@gmail.com> wrote:
Which means that, fundamentally, EAFP is the way to do it. So if PEP 463 expressions had existed from the beginning, hasattr() probably wouldn't have been written - people would just use an except-expression instead.
Really? hasattr() is much easier to write than the corresponding except-expression.
But would it be sufficiently easier to justify the creation of a built-in?
Well, can you propose the corresponding except-expression?
It's hard to do hasattr itself without something messy - the best I can come up with is this: hasattr(x,"y") <-> (x.y or True except AttributeError: False) but the bulk of uses of it are immediately before attempting to use the attribute. Many require multiple statements, so they'd be better done as a full try/except: cpython/python-gdb.py:1392: if not hasattr(self._gdbframe, 'select'): print ('Unable to select frame: ' 'this build of gdb does not expose a gdb.Frame.select method') return False self._gdbframe.select() return True becomes try: self._gdbframe.select() return True except AttributeError: print ('Unable to select frame: ' 'this build of gdb does not expose a gdb.Frame.select method') return False but others are clearly expressions in disguise: Lib/aifc.py:882: if hasattr(f, 'mode'): mode = f.mode else: mode = 'rb' becomes mode = (f.mode except AttributeError: 'rb') (In fact, I'm adding that one to the PEP's examples section.) Lib/cgi.py:145: if hasattr(fp,'encoding'): encoding = fp.encoding else: encoding = 'latin-1' becomes encoding = (fp.encoding except AttributeError: 'latin-1') Some could be done either way. If hasattr didn't exist, then this: Lib/argparse.py:597: if hasattr(params[name], '__name__'): params[name] = params[name].__name__ could be written instead as: params[name] = (params[name].__name__ except AttributeError: params[name]) which is similar length and doesn't require a built-in. Some are fairly clearly asking to be done as try/except, irrespective of this PEP: Lib/decimal.py:449: if hasattr(threading.current_thread(), '__decimal_context__'): del threading.current_thread().__decimal_context__ becomes try: del threading.current_thread().__decimal_context__ except AttributeError: pass (also ibid:476) Some are a bit of a mixture. Lib/dis.py:40: if hasattr(x, '__func__'): # Method x = x.__func__ if hasattr(x, '__code__'): # Function x = x.__code__ if hasattr(x, '__dict__'): # Class or module ... lots more code ... Could be done as try/except; first part could be done cleanly as an expression. This one's not quite as clean, but if hasattr didn't exist, this could be done either of two ways: Lib/fileinput.py:342: if hasattr(os, 'O_BINARY'): mode |= os.O_BINARY As an expression: mode |= (os.O_BINARY except AttributeError: 0) Or as a statement: try: mode |= os.O_BINARY except AttributeError: pass This one definitely would want to be changed, and is also going in the PEP: Lib/inspect.py:1350: return sys._getframe(1) if hasattr(sys, "_getframe") else None becomes return (sys._getframe(1) except AttributeError: None) Lib/ntpath.py:558: # Win9x family and earlier have no Unicode filename support. supports_unicode_filenames = (hasattr(sys, "getwindowsversion") and sys.getwindowsversion()[3] >= 2) becomes supports_unicode_filenames = (sys.getwindowsversion()[3] >= 2 except AttributeError: False) Another ternary-if LBYL that could become an expression-except EAFP: Lib/pdb.py:745: globs = self.curframe.f_globals if hasattr(self, 'curframe') else None becomes globs = (self.curframe.f_globals except AttributeError: None) although that will return None if self.curframe has no f_globals. Another nice easy one: Lib/pickletools.py:2227: if hasattr(data, "tell"): getpos = data.tell else: getpos = lambda: None becomes getpos = (data.tell except AttributeError: lambda: None) I could continue this theme, but behold, as Rose Maybud said, I have said enough. There are definitely cases where a local hasattr function could be useful, but if the code were already written to use try/except or an except-expression, there aren't many that would justify the creation of a builtin. ChrisA
On Sat, 22 Feb 2014 22:13:58 +1100 Chris Angelico <rosuav@gmail.com> wrote:
Well, can you propose the corresponding except-expression?
It's hard to do hasattr itself without something messy - the best I can come up with is this:
hasattr(x,"y") <-> (x.y or True except AttributeError: False)
But it's not the same. hasattr() returns a boolean, not an arbitrary value.
try: self._gdbframe.select() return True except AttributeError: print ('Unable to select frame: ' 'this build of gdb does not expose a gdb.Frame.select method') return False
But then you can't distinguish between a missing "select" method and an AttributeError raised by self._gdbframe.select() itself.
but others are clearly expressions in disguise:
Lib/aifc.py:882: if hasattr(f, 'mode'): mode = f.mode else: mode = 'rb' becomes mode = (f.mode except AttributeError: 'rb')
Not significantly less wordy. Note you can already write: mode = getattr(f, 'mode', 'rb') which is more concise.
(In fact, I'm adding that one to the PEP's examples section.)
Lib/cgi.py:145: if hasattr(fp,'encoding'): encoding = fp.encoding else: encoding = 'latin-1' becomes encoding = (fp.encoding except AttributeError: 'latin-1')
Same here: encoding = getattr(fp, 'encoding', 'latin-1')
Some could be done either way. If hasattr didn't exist, then this:
Lib/argparse.py:597: if hasattr(params[name], '__name__'): params[name] = params[name].__name__
could be written instead as: params[name] = (params[name].__name__ except AttributeError: params[name])
This makes a useless assignment if the attribute doesn't exist; it also spans a single expression over two lines instead of having two simple statements. It's definitely worse IMO.
Some are fairly clearly asking to be done as try/except, irrespective of this PEP:
Lib/decimal.py:449: if hasattr(threading.current_thread(), '__decimal_context__'): del threading.current_thread().__decimal_context__ becomes try: del threading.current_thread().__decimal_context__ except AttributeError: pass
May hide a bug if threading.current_thread doesn't exist.
Lib/inspect.py:1350: return sys._getframe(1) if hasattr(sys, "_getframe") else None becomes return (sys._getframe(1) except AttributeError: None)
May hide a bug if sys._getframe(1) itself raises AttributeError. (etc.)
I could continue this theme, but behold, as Rose Maybud said, I have said enough.
Yeah, none of these examples makes a convincing case that hasattr() is useless, IMO. Regards Antoine.
On Sat, Feb 22, 2014 at 10:27 PM, Antoine Pitrou <solipsis@pitrou.net> wrote:
Yeah, none of these examples makes a convincing case that hasattr() is useless, IMO.
I'm not trying to make the case that it's useless. I'm trying to show that, if it didn't exist, all of these would be written some other way, and the case for its creation would not be terribly strong. It's definitely of value (and as you've shown in some of those cases, its proper use can narrow the exception-catching scope - a good thing), but not enough to be worth blessing with a built-in function. ChrisA
Antoine Pitrou writes:
On sam., 2014-02-22 at 19:29 +0900, Stephen J. Turnbull wrote:
Antoine Pitrou writes:
Well, the only way to know that a key (or attribute) exists is to do the lookup. What else would you suggest?
Do the lookup at the C level (or whatever the implementation language is) and generate no exception, of course. That's what would make it possibly more efficient.
Let's see: - hasattr() does the lookup at the C level, and silences the AttributeError - dict.get() does the lookup at the C level, and doesn't generate an exception
So apart from the minor inefficiency of generating and silencing the AttributeError, those functions already do what you suggest.
But that's precisely the inefficiency I'm referring to.
On sam., 2014-02-22 at 20:54 +0900, Stephen J. Turnbull wrote:
Antoine Pitrou writes:
On sam., 2014-02-22 at 19:29 +0900, Stephen J. Turnbull wrote:
Antoine Pitrou writes:
Well, the only way to know that a key (or attribute) exists is to do the lookup. What else would you suggest?
Do the lookup at the C level (or whatever the implementation language is) and generate no exception, of course. That's what would make it possibly more efficient.
Let's see: - hasattr() does the lookup at the C level, and silences the AttributeError - dict.get() does the lookup at the C level, and doesn't generate an exception
So apart from the minor inefficiency of generating and silencing the AttributeError, those functions already do what you suggest.
But that's precisely the inefficiency I'm referring to.
Sure, but complaining about inefficiencies without asserting their significance is not very useful. Regards Antoine.
Antoine Pitrou writes:
On Sat, 22 Feb 2014 22:13:58 +1100 Chris Angelico <rosuav@gmail.com> wrote:
hasattr(x,"y") <-> (x.y or True except AttributeError: False)
But it's not the same. hasattr() returns a boolean, not an arbitrary value.
I think he meant hasattr(x,"y") <-> (x.y and True except AttributeError: False)
On 2/22/2014 6:27 AM, Antoine Pitrou wrote:
On Sat, 22 Feb 2014 22:13:58 +1100 Chris Angelico <rosuav@gmail.com> wrote:
Lib/inspect.py:1350: return sys._getframe(1) if hasattr(sys, "_getframe") else None becomes return (sys._getframe(1) except AttributeError: None)
May hide a bug if sys._getframe(1) itself raises AttributeError.
return (sys._getframe except AttributeError: lambda i: None)(1) Assuming I have the syntax correct, and the bindings work this way, of course.
On Sat, Feb 22, 2014 at 11:14 PM, Stephen J. Turnbull <stephen@xemacs.org> wrote:
Antoine Pitrou writes:
On Sat, 22 Feb 2014 22:13:58 +1100 Chris Angelico <rosuav@gmail.com> wrote:
hasattr(x,"y") <-> (x.y or True except AttributeError: False)
But it's not the same. hasattr() returns a boolean, not an arbitrary value.
I think he meant
hasattr(x,"y") <-> (x.y and True except AttributeError: False)
No, I meant 'or' to ensure that an attribute holding a false value doesn't come up false. But if you really want a boolean, just wrap it up in bool(). My main point, though, was that most usage of hasattr is probing just before something gets used - something like this: if hasattr(obj, "attr"): blah blah obj.attr else: maybe use a default or maybe do nothing Some cases could become except-expressions; others could become try/except statements. In each case, the loss would be small. I'm not saying hasattr should be removed, just that it wouldn't have a strong case if it didn't already exist. ChrisA
On 22 Feb 2014 22:15, "Stephen J. Turnbull" <stephen@xemacs.org> wrote:
Antoine Pitrou writes:
On Sat, 22 Feb 2014 22:13:58 +1100 Chris Angelico <rosuav@gmail.com> wrote:
hasattr(x,"y") <-> (x.y or True except AttributeError: False)
But it's not the same. hasattr() returns a boolean, not an arbitrary value.
I think he meant
hasattr(x,"y") <-> (x.y and True except AttributeError: False)
With PEP 463, the explicit equivalent of hasattr() would be something like : hasattr(x,"y") <-> (bool(x.y) or True except AttributeError: False) The version Chris came up with was close, but as Antoine noted, didn't ensure the result was always exactly True or False. The translation isn't simple because we don't allow an "else" clause on the except expression (and I agree with this limitation), so the first expression needs to be one that will *evaluate* x.y, but ensure the result of the expression is True if no exception is thrown. However, as Chris noted in his reply, there are still cases where using hasattr makes more sense, so the fact it *can* be written as an except expression instead is a curiosity rather than anything with deep practical implications. Cheers, Nick. P.S. The fully general variant of "else" emulation under PEP 463: ((bool(EXPR) and False) or NO_EXC_RESULT except EXC: EXC_RESULT) Note: never actually use this, it's effectively unreadable ;)
_______________________________________________
Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/ncoghlan%40gmail.com
On Fri, Feb 21, 2014 at 8:41 PM, Greg Ewing <greg.ewing@canterbury.ac.nz>wrote:
Ethan Furman wrote:
On 02/21/2014 03:29 PM, Greg Ewing wrote:
value = lst[2] except "No value" if IndexError
It does read nicely, and is fine for the single, non-nested, case (which is probably the vast majority), but how would it handle nested exceptions?
Hmmm, probably not very well, unless we define
a except b if E1 except c if E2
to mean
a except (b except c if E2) if E1
If E1 == E2, that could perhaps be abbreviated to
a except b except c if E
Or we could just decide that the nested case is going to be so rare it's not worth worrying about.
+1 on not caring. Keep the expression for simple, obvious cases of a single exception type and fall back to the statement for fancier use (just like listcomps). The focus should be ease of expressiveness for a common pattern, not trying to convert tons of try/except statements into an expression just because we can.
On Sat, Feb 22, 2014 at 4:13 AM, Antoine Pitrou <solipsis@pitrou.net> wrote:
On Fri, 21 Feb 2014 09:37:29 -0800 Guido van Rossum <guido@python.org> wrote:
I'm put off by the ':' syntax myself (it looks to me as if someone forgot a newline somewhere) but 'then' feels even weirder (it's been hard-coded in my brain as meaning the first branch of an 'if').
Would 'else' work rather than 'then'?
thing = stuff['key'] except KeyError else None That reads to me like the exception was silenced and only if there is no exception the None is returned, just like an 'else' clause on a 'try' statement. I personally don't mind the 'then' as my brain has been hard-coded to mean "the first branch of a statement" so it's looser than being explicitly associated with 'if' but with any multi-clause statement.
On 22/02/2014 16:36, Brett Cannon wrote:
On Sat, Feb 22, 2014 at 4:13 AM, Antoine Pitrou <solipsis@pitrou.net <mailto:solipsis@pitrou.net>> wrote:
On Fri, 21 Feb 2014 09:37:29 -0800 Guido van Rossum <guido@python.org <mailto:guido@python.org>> wrote: > I'm put off by the ':' syntax myself (it looks to me as if someone forgot a > newline somewhere) but 'then' feels even weirder (it's been hard-coded in > my brain as meaning the first branch of an 'if').
Would 'else' work rather than 'then'?
thing = stuff['key'] except KeyError else None
That reads to me like the exception was silenced and only if there is no exception the None is returned, just like an 'else' clause on a 'try' statement.
I personally don't mind the 'then' as my brain has been hard-coded to mean "the first branch of a statement" so it's looser than being explicitly associated with 'if' but with any multi-clause statement.
I read *except* as 'except if', and *:* as 'then' (often), so the main proposal reads naturally to me. I'm surprised to find others don't also, as that's the (only?) pronunciation that makes the familiar if-else and try-except constructs approximate English. Isn't adding a new keyword (*then*) likely to be a big deal? There is the odd example of its use as an identifier, just in our test code: http://hg.python.org/cpython/file/0695e465affe/Lib/test/test_epoll.py#l168 http://hg.python.org/cpython/file/0695e465affe/Lib/test/test_xmlrpc.py#l310 Jeff Allen
"Stephen J. Turnbull" <stephen@xemacs.org> writes:
Ethan Furman writes:
On 02/21/2014 07:46 PM, Chris Angelico wrote:
but not this:
value = expr except Exception: default except Exception: default
This should be the way it works. Nothing is gained in readability by turning a try with multiple except statements into an expression.
Examples have been given several times. In general, if 'expr' is a function call, it may well have a couple of different ways to fail which imply different default values.
interpolable = func(key) except TypeError: "not a string: %s" % key \ except KeyError: "no such key: %s" % key print("Some message that refers to '%s' % interpolable")
versus
try: interpolable = func(key) except TypeError: interpolable = "not a string: %s" % key except KeyError: interpolable = "no such key: %s" % key print("Some message that refers to '%s' % interpolable")
I think the following suggestion from elsewhere in the thread would look even better in this case: interpolable = func(key) except (TypeError: "not a string: %s" % key, KeyError: "no such key: %s" % key) print("Some message that refers to '%s' % interpolable") It does not require the backslash, it is shorter, and it can still be chained: interpolable = func(key) except (TypeError: "not a string: %s" % key, KeyError: defaults[key] except (KeyError: "no such key: %s" % key)) print("Some message that refers to '%s' % interpolable") Best, -Nikolaus -- Encrypted emails preferred. PGP fingerprint: 5B93 61F8 4EA2 E279 ABF6 02CF A9AD B7F8 AE4E 425C »Time flies like an arrow, fruit flies like a Banana.«
On 23 February 2014 02:29, Nick Coghlan <ncoghlan@gmail.com> wrote:
On 22 Feb 2014 22:15, "Stephen J. Turnbull" <stephen@xemacs.org> wrote:
Antoine Pitrou writes:
Chris Angelico <rosuav@gmail.com> wrote:
hasattr(x,"y") <-> (x.y or True except AttributeError: False) But it's not the same. hasattr() returns a boolean, not an arbitrary value. I think he meant hasattr(x,"y") <-> (x.y and True except AttributeError: False)
With PEP 463, the explicit equivalent of hasattr() would be something like :
hasattr(x,"y") <-> (bool(x.y) or True except AttributeError: False)
That would work, but I think I'd prefer: hasattr(x,"y") <-> bool(x.y or True except AttributeError: False) Makes it clearer IMO that the entire expression will always return a boolean. If exception expressions already existed in the language, I would think there would be a strong argument for a library function hasattr(), but probably not a builtin. Tim Delaney
Antoine Pitrou writes:
Sure, but complaining about inefficiencies without asserting their significance is not very useful.
Since you completely missed the point of my post, I'll explain. I was in no way complaining about inefficiencies. My point was precisely the opposite: to the extent that most 'get' methods would be implemented in Python, even the minor inefficiency of creating and suppressing a useless exception can't be avoided with the LBYL of a get method, because haveattr itself is implemented by generating and suppressing an exception. I know that it's not a big deal[1], but it did help swing me to positive on this PEP. Footnotes: [1] If it were, somebody would have reimplemented haveattr to avoid generating an exception.
On Sat, Feb 22, 2014 at 2:08 AM, Thomas Wouters <thomas@python.org> wrote:
(FWIW, I have a working patch without tests that allows all of these, I'll upload it tonight so people can play with it. Oh, and FWIW, currently I'm +0 on the idea, -0 on the specific syntax.)
http://bugs.python.org/issue20739 is the patch. -- Thomas Wouters <thomas@python.org> Hi! I'm an email virus! Think twice before sending your email to help me spread!
On Sun, Feb 23, 2014 at 11:00 AM, Thomas Wouters <thomas@python.org> wrote:
On Sat, Feb 22, 2014 at 2:08 AM, Thomas Wouters <thomas@python.org> wrote:
(FWIW, I have a working patch without tests that allows all of these, I'll upload it tonight so people can play with it. Oh, and FWIW, currently I'm +0 on the idea, -0 on the specific syntax.)
http://bugs.python.org/issue20739 is the patch.
Thanks! You make a comment about precedence. When I wrote that up, it was basically just "that seems about right"; whether it's equal to lambda, equal to if/else, above both, below both, or in between, is free to be tweaked according to what makes sense. Nobody has to date discussed the exact precedence order, so feel free to tweak it for the benefit of implementation. ChrisA
On 2014-02-23 00:09, Chris Angelico wrote:
On Sun, Feb 23, 2014 at 11:00 AM, Thomas Wouters <thomas@python.org> wrote:
On Sat, Feb 22, 2014 at 2:08 AM, Thomas Wouters <thomas@python.org> wrote:
(FWIW, I have a working patch without tests that allows all of these, I'll upload it tonight so people can play with it. Oh, and FWIW, currently I'm +0 on the idea, -0 on the specific syntax.)
http://bugs.python.org/issue20739 is the patch.
Thanks!
You make a comment about precedence. When I wrote that up, it was basically just "that seems about right"; whether it's equal to lambda, equal to if/else, above both, below both, or in between, is free to be tweaked according to what makes sense. Nobody has to date discussed the exact precedence order, so feel free to tweak it for the benefit of implementation.
My feeling is that catching exceptions should have a lower precedence than the other parts of an expression, but higher than comma, so: A if C else B except E: D would be parsed as: (A if C else B) except E: D I think that's because it's kind of replacing: try: _ = expr except E: _ = D with the try..except enclosing the expression.
On 23 February 2014 11:11, MRAB <python@mrabarnett.plus.com> wrote:
On 2014-02-23 00:09, Chris Angelico wrote:
On Sun, Feb 23, 2014 at 11:00 AM, Thomas Wouters <thomas@python.org> wrote:
On Sat, Feb 22, 2014 at 2:08 AM, Thomas Wouters <thomas@python.org> wrote:
(FWIW, I have a working patch without tests that allows all of these, I'll upload it tonight so people can play with it. Oh, and FWIW, currently I'm +0 on the idea, -0 on the specific syntax.)
http://bugs.python.org/issue20739 is the patch.
Thanks!
You make a comment about precedence. When I wrote that up, it was basically just "that seems about right"; whether it's equal to lambda, equal to if/else, above both, below both, or in between, is free to be tweaked according to what makes sense. Nobody has to date discussed the exact precedence order, so feel free to tweak it for the benefit of implementation.
My feeling is that catching exceptions should have a lower precedence than the other parts of an expression, but higher than comma, so:
A if C else B except E: D
would be parsed as:
(A if C else B) except E: D
I think that's because it's kind of replacing:
try: _ = expr except E: _ = D
with the try..except enclosing the expression.
Note that mandatory parentheses means we can duck the precedence question entirely, which I count as another point in favour of requiring them :) Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Sun, 23 Feb 2014, Nick Coghlan wrote:
Note that mandatory parentheses means we can duck the precedence question entirely, which I count as another point in favour of requiring them :)
Careful, if you take that too far then Python 4 will have to be Scheme. ;-) Isaac Morland CSCF Web Guru DC 2619, x36650 WWW Software Specialist
Chris Angelico, 21.02.2014 04:15:
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. [...] 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"
I see a risk of interfering with in-place assignment operators, e.g. x /= y except ZeroDivisionError: 1 might not do what one could expect, because (as I assume) it would behave differently from x = x / y except ZeroDivisionError: 1 I think that falls under the "overly broad exception handling" issue. If you want to include the assignment, you'll have to spell out the try-except block yourself. I find the difference in the two behaviours very unfortunate, though. This also reduces the scope of applicability somewhat. Cython has typed assignments, so a straight forward idea would be to handle TypeErrors in assignments like this: cdef str s s = x except TypeError: str(x) However, I guess that would similarly counter the idea of exception handling in an *expression*, and the correct and non-ambiguous way to do this would be to spell out the try-except block. Summing it up, my impression is that it helps some use cases but leaves others more ambiguous/unclear/unfortunate, which makes me lean towards rejecting it. Stefan
Stefan Behnel, 23.02.2014 19:51:
Cython has typed assignments, so a straight forward idea would be to handle TypeErrors in assignments like this:
cdef str s s = x except TypeError: str(x)
Similar code in Python would be this: from array import array x = array('i', [1,2,3]) value = "123" x[0] = value except TypeError: int(value)
However, I guess that would similarly counter the idea of exception handling in an *expression*, and the correct and non-ambiguous way to do this would be to spell out the try-except block.
Stefan
On Feb 23, 2014 7:52 PM, "Stefan Behnel" <stefan_ml@behnel.de> wrote:
Chris Angelico, 21.02.2014 04:15:
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. [...] 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"
I see a risk of interfering with in-place assignment operators, e.g.
x /= y except ZeroDivisionError: 1
might not do what one could expect, because (as I assume) it would behave differently from
x = x / y except ZeroDivisionError: 1
Yes. Augmented assignment is still assignment, so a statement. The only way to parse that is as x /= (y except ZeroDivisionError: 1) and it'd be equivalent to x = x / (y except ZeroDivisionError: 1) (If the parentheses are mandatory that makes it easier to spot the difference.)
I think that falls under the "overly broad exception handling" issue. If you want to include the assignment, you'll have to spell out the
try-except
block yourself. I find the difference in the two behaviours very unfortunate, though.
This also reduces the scope of applicability somewhat. Cython has typed assignments, so a straight forward idea would be to handle TypeErrors in assignments like this:
cdef str s s = x except TypeError: str(x)
However, I guess that would similarly counter the idea of exception handling in an *expression*, and the correct and non-ambiguous way to do this would be to spell out the try-except block.
Summing it up, my impression is that it helps some use cases but leaves others more ambiguous/unclear/unfortunate, which makes me lean towards rejecting it.
Stefan
_______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/thomas%40python.org
On 02/23/2014 11:26 AM, Thomas Wouters wrote:
On Feb 23, 2014 7:52 PM, "Stefan Behnel" <stefan_ml@behnel.de <mailto:stefan_ml@behnel.de>> wrote:
Chris Angelico, 21.02.2014 04:15:
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. [...] 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"
I see a risk of interfering with in-place assignment operators, e.g.
x /= y except ZeroDivisionError: 1
might not do what one could expect, because (as I assume) it would behave differently from
x = x / y except ZeroDivisionError: 1
Yes. Augmented assignment is still assignment, so a statement. The only way to parse that is as
x /= (y except ZeroDivisionError: 1)
Well, that is certainly not what I would have expected. I can also see how parentheses can help, but I still would like them optional. -- ~Ethan~
On Mon, Feb 24, 2014 at 6:26 AM, Thomas Wouters <thomas@python.org> wrote:
I see a risk of interfering with in-place assignment operators, e.g.
x /= y except ZeroDivisionError: 1
might not do what one could expect, because (as I assume) it would behave differently from
x = x / y except ZeroDivisionError: 1
Yes. Augmented assignment is still assignment, so a statement. The only way to parse that is as
x /= (y except ZeroDivisionError: 1)
and it'd be equivalent to
x = x / (y except ZeroDivisionError: 1)
(If the parentheses are mandatory that makes it easier to spot the difference.)
I think that falls under the "overly broad exception handling" issue. If you want to include the assignment, you'll have to spell out the try-except block yourself. I find the difference in the two behaviours very unfortunate, though.
Thomas's analysis is correct. It's not overly broad; in fact, what you have is an overly _narrow_ exception handler, catching a ZeroDivisionError from the evaluation of y only. ChrisA
23.02.2014 19:51, Stefan Behnel wrote:
I see a risk of interfering with in-place assignment operators, e.g.
x /= y except ZeroDivisionError: 1
might not do what one could expect, because (as I assume) it would behave differently from
x = x / y except ZeroDivisionError: 1 [snip]
Please note that: x /= y if y else 0 also behaves differently from x = x / y if y else 0 Anyway, enclosing in parens would make that expicit and clear. Cheers. *j
On Mon, Feb 24, 2014 at 7:51 AM, Ethan Furman <ethan@stoneleaf.us> wrote:
Yes. Augmented assignment is still assignment, so a statement. The only way to parse that is as
x /= (y except ZeroDivisionError: 1)
Well, that is certainly not what I would have expected.
I can see that you'd want to have that go back and redo the division with a second argument of 1, which'd look like this in statement form: try: x /= y except ZeroDivisionError: x /= 1 But, just like the decried error suppression technique, the second half of this is something that should instead be written "pass". At very least, I'd say that an except-expression where one or other of its forms is better spelled "pass" is code smell, and at worst, I'd say it's a hint that the expression form might not even be what you think it is - as in this case. Remember, this is a scope-narrowing. Where previously you had to try/except entire statements, now you can try/except just one part of something. That means you won't catch errors in the actual assignment - which is usually a good thing - but it does affect augmented assignment. My recommendation: Just use try... except pass. I'm not trying to supplant the statement form :) ChrisA
On 22/02/2014 21:26, Tim Delaney wrote:
On 23 February 2014 02:29, Nick Coghlan <ncoghlan@gmail.com <mailto:ncoghlan@gmail.com>> wrote:
On 22 Feb 2014 22:15, "Stephen J. Turnbull" <stephen@xemacs.org <mailto:stephen@xemacs.org>> wrote: > Antoine Pitrou writes: > > Chris Angelico <rosuav@gmail.com <mailto:rosuav@gmail.com>> wrote: > > > hasattr(x,"y") <-> (x.y or True except AttributeError: False) > > But it's not the same. hasattr() returns a boolean, not an arbitrary > > value. > I think he meant > hasattr(x,"y") <-> (x.y and True except AttributeError: False)
With PEP 463, the explicit equivalent of hasattr() would be something like :
hasattr(x,"y") <-> (bool(x.y) or True except AttributeError: False)
That would work, but I think I'd prefer:
hasattr(x,"y") <-> bool(x.y or True except AttributeError: False)
Makes it clearer IMO that the entire expression will always return a boolean.
This also works: hasattr(x,"y") <-> (lambda v: True)(x.y) except AttributeError: False Which is less obscure is a matter of judgement.
Some of your points have been answered by others, I'll try to avoid repetition. On 21/02/2014 19:04, Yury Selivanov wrote:
[snip]
Inconvenience of dict[] raising KeyError was solved by introducing the dict.get() method. And I think that
dct.get('a', 'b')
is 1000 times better than
dct['a'] except KeyError: 'b' Do you? I don't. "Explicit is better than implicit". I think this may be partly a matter of familiarity.
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
# Lib/tarfile.py:2198: try: g = grp.getgrnam(tarinfo.gname)[2] except KeyError: g = tarinfo.gid try: u = pwd.getpwnam(tarinfo.uname)[2] except KeyError: u = tarinfo.uid This one is a valid example, but totally unparseable by humans. Again, I find the more concise version easier to read than the original. It is dense, yes. It takes time to read and absorb, sure - but so does the original 8-line version. And it makes the repetition of the same code structure much more obvious.
I think all of the above more readable with try statement.
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", _)
If you replace '_' with a 'msg' (why did you use '_'??) then try statements are *much* more readable.
[snip]
Lib/ipaddress.py:343:: try: ips.append(ip.ip) except AttributeError: ips.append(ip.network_address) Becomes:: ips.append(ip.ip except AttributeError: ip.network_address) or it may become:
ips.append(getattr(ip, 'ip', ip.network_address))
or
address = getattr(ip, 'ip', ip.network_address) ips.append(address) Please note that any of these is an improvement on the original, in that
I think there is a consensus that this was a poor example, unless intended to show how the new construction (like any other) could be abused to produce hard-to-understand code. they don't trap an AttributeError evaluating ips.append. (The old try ... except syntax can be used incorrectly, just as any new syntax can.)
---
All in all, your proposal scares me. I doesn't make python code readable,
Again, a personal judgement.
it doesn't solve the problem of overbroad exceptions handling (you have couple examples of overbroad handling in your PEP examples section).
Yes, some examples look neat. But your syntax is much easier to abuse, than 'if..else' expression, Why? Any syntax can be abused. I can easily abuse if..else if I want to: ErrorMessage = (None if eggs else "No eggs") if ham else "No ham" if eggs else "No ham or eggs" # Tested
Suppose you were learning Python. Which is easier, to learn lots of special methods (dict.get, getattr etc.), or to learn ONE self-explanatory form of syntax that covers them all: Dict[Key] except KeyError: default List[Index] except IndexError: default x.y except AttributeError: default (Of course, I'm not saying "Don't use getattr". Just that you could get by if you've never heard of it.)
On 21/02/2014 23:36, Ethan Furman wrote:
On 02/21/2014 02:26 PM, Eric V. Smith wrote:
On 2/21/2014 5:06 PM, Greg Ewing wrote:
On 21 February 2014 13:15, Chris Angelico wrote:
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
There would be no ambiguity if only nested excepts are allowed. If one wants to catch multiple exceptions from one expression, /and do something different for each one/, use the statement form as it's going to be clearer. For example:
try: value = 1/x except ZeroDivisionError: try: value = 1/default['denominator'] except KeyError: value = NaN
is much cleaner as:
value = 1/x except ZeroDivisionError: 1/default['denominator'] except KeyError: NaN
However, this:
try: result = Parse(some_stuff) except MissingOperator: result = ... except InvalidOperand: result = ... except SomethingElse: result = ...
would not benefit from being condensed into a single expression
-- ~Ethan~ Funny, my feeling was exactly the reverse. :-) Probably because the latter seems to me to be a more natural thing to want to do (I find it easier to imagine use cases for it). And also because there is no way of getting exactly the same effect with a parenthesized except-expression: (expr except ValueError: ValueErrrorMessage") except NameError:NameErrorMessage # doesn't quite do what I want (here if expr raises a ValueError, evaluating ValueErrrorMessage which is mis-spelt will raise a NameError which will be misleadingly caught). But I guess the truth is that any except-expression which gets too long and complicated should be written some other way.
On 2/21/2014 5:06 PM, Jan Kaliszewski wrote:
Or even (still being my favorite):
msg = seq[i] except (IndexError: "nothing")
This syntax actually has a benefit: the parenthesized syntax after except could become a list, to allow handling different exceptions from the tried expression with different results:
msg = seq[dictionary[i]] except (IndexError: "nothing", KeyError: "serious problems") It shouldn't be a true list. We need lazy evaluation of the default values. And if an unlisted exception is raised, we don't want any of
On 22/02/2014 02:08, Glenn Linderman wrote: the defaults evaluated. Rob Cliffe
And still allows nesting:
msg = seq[i] except (IndexError: dictionary[i] except (KeyError: "no fallback data for %s" % i))
_______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/rob.cliffe%40btinternet.c...
No virus found in this message. Checked by AVG - www.avg.com <http://www.avg.com> Version: 2012.0.2247 / Virus Database: 3705/6616 - Release Date: 02/22/14
Yury Selivanov wrote:
I think the Motivation section is pretty weak.
I have normally wished for this when I was (semi- interactively) exploring a weakly structured dataset. Often, I start with a string, split it into something hopefully like records, and then start applying filters and transforms. I would prefer to write a comprehension instead of a for loop. Alas, without pre-editing, I can be fairly confident that the data is dirty. Sometimes I can solve it with a filter (assuming that I remember and don't mind the out-of-order evaluation): # The "if value" happens first, # so the 1/value turns out to be safe. [1/value for value in working_list if value] Note that this means dropping the bad data, so that items in this list will have different indices than those in the parent working_list. I would rather have written: [1/value except (TypeError, ZeroDivisionError): None] which would keep the matching indices, and clearly indicate where I now had missing/invalid data. Sometimes I solve it with a clumsy workaround: sum((e.weight if hasattr(e, 'weight') else 1.0) for e in working_list) But the "hasattr" implies that I am doing some sort of classification based on whether or not the element has a weight. The true intent was to recognize that while every element does have a weight, the representation that I'm starting from didn't always bother to store it -- so I am repairing that before processing. sum(e.weight except AttributeError: 1) Often I give up, and create a junky helper function, or several. But to avoid polluting the namespace, I may leave it outside the class, or give it a truly bad name: def __only_n2(worklist): results = [] for line in worklist: line=line.strip() if not line: # or maybe just edit the input file... continue split1=line.split(", ") if 7 != len(split1): continue if "n2" == split1[3]: results.append(split1) return results worklist_n2 = __only_n2(worklist7) In real life code, even after hand-editing the input data to fix a few cases, I recently ended up with: class VoteMark: ... @classmethod def from_property(cls, voteline): # print (voteline) count, _junk, prefs = voteline.partition(": ") return cls(count, prefs) ... # module level scope def make_votes(vs=votestring): return [VoteMark.from_property(e) for e in vs.splitlines()] vs=make_votes() You can correctly point out that I was being sloppy, and that I *should* have gone back to clean it up. But I wouldn't have had to clean up either the code or the data (well, not as much), if I had been able to just keep the step-at-a-time transformations I was building up during development: vs=[(VoteMark(*e.strip().split(": ")) except (TypeError, ValueError): None) for e in votestring.splitlines()] Yes, the first line is still doing too much, and might be worth a helper function during cleanup. But it is already better than an alternate constructor that exists only to simplify a single (outside the class) function that is only called once. Which in turn is better than the first draft that was so ugly that I actually did fix it during that same work session.
Inconvenience of dict[] raising KeyError was solved by introducing the dict.get() method. And I think that
dct.get('a', 'b')
is 1000 times better than
dct['a'] except KeyError: 'b'
I don't. dct.get('a', default='b') would be considerably better, but it would still imply that missing values are normal. So even after argclinic is fully integrated, there will still be times when I prefer to make it explicit that I consider this an abnormal case. (And, as others have pointed out, .get isn't a good solution when the default is expensive to compute.)
Consider this example of a two-level cache:: for key in sequence: x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key)))
I'm sorry, it took me a minute to understand what your example is doing. I would rather see two try..except blocks than this.
Agreed -- like my semi-interactive code above, it does too much on one line. I don't object as much to: for key in sequence: x = (lvl1[key] except KeyError: (lvl2[key] except KeyError: f(key)))
Retrieve an argument, defaulting to None:: cond = args[1] except IndexError: None
# Lib/pdb.py:803: try: cond = args[1] except IndexError: cond = None
cond = None if (len(args) < 2) else args[1]
This is an area where tastes will differ. I view the first as saying that not having a cond would be unusual, or at least a different kind of call. I view your version as a warning that argument parsing will be complex, and that there may be specific combinations of arguments that are only valid depending on the values of other arguments. Obviously, not everyone will share that intuition, but looking at the actual code, the first serves me far better. (It is a do_condition method, and falsy values -- such as None -- trigger a clear rather than a set.)
Attempt a translation, falling back on the original:: e.widget = self._nametowidget(W) except KeyError: W
# Lib/tkinter/__init__.py:1222: try: e.widget = self._nametowidget(W) except KeyError: e.widget = W
Note that if the value were being stored in a variable instead of an attribute, it would often be written more like: try: W=_nametowidget(W) except: pass
I'm not sure this is a good example either. ... Your new syntax just helps to work with this error prone api.
Exactly. You think that is bad, because it encourages use of a sub-optimal API. I think it is good, because getting an external API fixed is ... unlikely to happen.
# sys.abiflags may not be defined on all platforms. _CONFIG_VARS['abiflags'] = sys.abiflags except AttributeError: ''
Ugly. _CONFIG_VARS['abiflags'] = getattr(sys, 'abiflags', '') Much more readable.
Again, tastes differ. To me, getattr looks too much like internals, and I wonder if I will need to look up getattribute or __getattr__, because maybe this code is doing something strange. After an unpleasant pause, I realize that, no, it really is safe. Then later, I wonder if _CONFIG_VARS is some odd mapping with special limits. Then I see that it doesn't matter, because that isn't what you're passing to getattr. Then I remember that sys often is a special case, and start wondering if I need extra tests around this. Wait, why was I looking at this code again? (And as others pointed out, getattr with a constant has its own code smell.) The "except" form clearly indicates that sys.abiflags *ought* to be there, and the code is just providing some (probably reduced) services to oddball systems, instead of failing.
Retrieve an indexed item, defaulting to None (similar to dict.get):: def getNamedItem(self, name): return self._attrs[name] except KeyError: None
_attrs there is a dict (or at least it's something that quaks like a dict, and has [] and keys()), so
return self._attrs.get(name)
Why do you assume it has .get? Or that .get does what a python mapping normally does with .get? The last time I dove into code like that, it was written that way specifically because the DOM (and _attrs in particular) might well be created by some other program, in support of a very unpythonic API. Note that the method itself is called getNamedItem, rather than just get; that also suggests an external source of API expectations.
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
# Lib/tarfile.py:2198: try: g = grp.getgrnam(tarinfo.gname)[2] except KeyError: g = tarinfo.gid try: u = pwd.getpwnam(tarinfo.uname)[2] except KeyError: u = tarinfo.uid
This one is a valid example, but totally unparseable by humans. Moreover, it promotes a bad pattern, as you mask KeyErrors in 'grp.getgrnam(tarinfo.gname)' call.
Do you find the existing try: except: code any better, or are you just worried that it doesn't solve enough of the problem? FWIW, I think it doesn't matter where the KeyError came from; even if the problem were finding the getgrnam method, the right answer (once deployed, as opposed to during development) would still be to fall back to the best available information.
Perform some lengthy calculations in EAFP mode, handling division by zero as a sort of sticky NaN::
value = calculate(x) except ZeroDivisionError: float("nan")
vs
try: value = calculate(x) except ZeroDivisionError: value = float("nan")
I think all of the above more readable with try statement.
I don't, though I would wrap the except. The calculation is important; everything else is boilerplate, and the more you can get it out of the way, the better.
Yes, some examples look neat. But your syntax is much easier to abuse, than 'if..else' expression, and if people start abusing it, Python will simply loose it's readability advantage.
This is also an argument for mandatory parentheses. ()-continuation makes it easier to wrap the except clause out of the way. (((( )) (() ()) ))-proliferation provides pushback when the expressions start to get too complicated. -jJ -- If there are still threading problems with my replies, please email me with details, so that I can try to resolve them. -jJ
Greg Ewing suggested:
This version might be more readable:
value = lst[2] except "No value" if IndexError
Ethan Furman asked:
It does read nicely, and is fine for the single, non-nested, case (which is probably the vast majority), but how would it handle nested exceptions?
With parentheses. Sometimes, the parentheses will make a complex expression ugly. Sometimes, a complex expression should really be factored into pieces anyway. Hopefully, these times are highly correlated. The above syntax does lend itself somewhat naturally to multiple *short* except clauses: value = (lst[2] except "No value" if IndexError except "Bad Input" if TypeError) and nested exception expressions are at least possible, but deservedly ugly: value = (lvl1[name] except (lvl2[name] except (compute_new_answer(name) except None if AppValueError) if KeyError) if KeyError) This also makes me wonder whether the cost of a subscope (for exception capture) could be limited to when an exception actually occurs, and whether that might lower the cost enough to make the it a good tradeoff. def myfunc1(a, b, e): assert "main scope e value" == e e = "main scope e value" value = (myfunc1(val1, val2, e) except e.reason if AppError as e) assert "main scope e value" == e -jJ -- If there are still threading problems with my replies, please email me with details, so that I can try to resolve them. -jJ
On Tue, Feb 25, 2014 at 11:27 AM, Jim J. Jewett <jimjjewett@gmail.com> wrote:
This also makes me wonder whether the cost of a subscope (for exception capture) could be limited to when an exception actually occurs, and whether that might lower the cost enough to make the it a good tradeoff.
def myfunc1(a, b, e): assert "main scope e value" == e
e = "main scope e value" value = (myfunc1(val1, val2, e) except e.reason if AppError as e) assert "main scope e value" == e
I'm sure it could. But there aren't many use-cases. Look at the one example I was able to find in the stdlib: http://legacy.python.org/dev/peps/pep-0463/#capturing-the-exception-object It's hardly a shining example of the value of the proposal. Got any really awesome demo that requires 'as'? Most of the supporting examples use something like KeyError where it's simply "was an exception thrown or not". ChrisA
On 21 Feb 2014, at 16:52, Chris Angelico <rosuav@gmail.com> wrote:
On Sat, Feb 22, 2014 at 1:34 AM, Brett Cannon <brett@python.org> wrote:
While I like the general concept, I agree that it looks too much like a crunched statement; the use of the colon is a non-starter for me. I'm sure I'm not the only one whose brain has been trained to view a colon in Python to mean "statement", period. This goes against that syntactic practice and just doesn't work for me.
I'm -1 with the current syntax, but it can go into the + range if a better syntax can be chosen.
We bikeshedded that extensively on -ideas. The four best options are:
value = (expr except Exception: default) value = (expr except Exception -> default) value = (expr except Exception pass default) value = (expr except Exception then default)
Note that the last option involves the creation of a new keyword.
Would any of the others feel better to you?
What about (also mentioned in the PEP)? value = (expr except Exception try default) This seems to read nicely, although “try” is at a completely different position than it is in the equivalent try statement. I like the general idea, but like Brett I don’t like using a colon here at all. Ronald P.S. Sorry if this way already brought up, I’ve browsed through most of the threads on this on -ideas and -dev, but haven’t read all messages.
ChrisA _______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/ronaldoussoren%40mac.com
On Thu, Feb 27, 2014 at 7:44 PM, Ronald Oussoren <ronaldoussoren@mac.com> wrote:
What about (also mentioned in the PEP)?
value = (expr except Exception try default)
This seems to read nicely, although “try” is at a completely different position than it is in the equivalent try statement.
I like the general idea, but like Brett I don’t like using a colon here at all.
I see your "although" clause to be quite a strong objection. In the statement form of an if, you have: if cond: true_suite else: false_suite In the expression form, you have: true_expr if cond else false_expr Personally, I think it's a weakness of the if-expression that they're not in the same order (cond, true_expr, false_expr), but they're still introduced with the same keywords. The 'if' keyword is followed by the condition, and the 'else' keyword by the false "stuff". Putting "try" followed by the default is confusing, because any exception raised in the default-expr will bubble up. Stealing any other keyword from the try/except block would make just as little sense: expr except Exception finally default # "finally" implies something that always happens expr except Exception else default # "else" implies *no* exception expr except Exception try default # "try" indicates the initial expr, not the default default except Exception try expr # breaks L->R evaluation order Left to right evaluation order is extremely important to me. I don't know about anyone else, but since I'm the one championing the PEP, you're going to have to show me a *really* strong incentive to reword it to advocate something like the last one :) This is stated in the PEP: http://www.python.org/dev/peps/pep-0463/#alternative-proposals Using try and except leaves the notation "mentally ambiguous" as to which of the two outer expressions is which. It doesn't make perfect sense either way, and I expect a lot of people would be flicking back to the docs constantly to make sure they had it right. It's hard when there's confusion across languages (try writing some code in REXX and Python that uses division and modulo operators - 1234/10 -> 123.4 in Py3 and REXX, but 1234//10 and 1234%10 have opposite meaning); it's unnecessarily hard to have the same confusion in different places in the same language. ChrisA
On 27 Feb 2014, at 11:09, Chris Angelico <rosuav@gmail.com> wrote:
On Thu, Feb 27, 2014 at 7:44 PM, Ronald Oussoren <ronaldoussoren@mac.com> wrote:
What about (also mentioned in the PEP)?
value = (expr except Exception try default)
This seems to read nicely, although “try” is at a completely different position than it is in the equivalent try statement.
I like the general idea, but like Brett I don’t like using a colon here at all.
I see your "although" clause to be quite a strong objection. In the statement form of an if, you have:
I’m not convinced that this is a strong objection. The order of keywords is different, but that doesn’t have to be problem.
if cond: true_suite else: false_suite
In the expression form, you have:
true_expr if cond else false_expr
[…]
Putting "try" followed by the default is confusing, because any exception raised in the default-expr will bubble up. Stealing any other keyword from the try/except block would make just as little sense:
expr except Exception finally default # "finally" implies something that always happens expr except Exception else default # "else" implies *no* exception expr except Exception try default # "try" indicates the initial expr, not the default
I didn’t parse the expression this way at all, but quite naturally parsed is as “use expr, and try using default if expr raises Exception” and not as a RTL expression.
default except Exception try expr # breaks L->R evaluation order
Left to right evaluation order is extremely important to me.
I agree with that, RTL evaluation would be pretty odd in Python.
I don't know about anyone else, but since I'm the one championing the PEP, you're going to have to show me a *really* strong incentive to reword it to advocate something like the last one :) This is stated in the PEP:
http://www.python.org/dev/peps/pep-0463/#alternative-proposals
Using try and except leaves the notation "mentally ambiguous" as to which of the two outer expressions is which. It doesn't make perfect sense either way, and I expect a lot of people would be flicking back to the docs constantly to make sure they had it right.
Really? The evaluation order you mention in above didn’t make sense to me until I tried to make sense of it. Ronald
On Thu, Feb 27, 2014 at 9:44 PM, Ronald Oussoren <ronaldoussoren@mac.com> wrote:
expr except Exception try default # "try" indicates the initial expr, not the default
I didn’t parse the expression this way at all, but quite naturally parsed is as “use expr, and try using default if expr raises Exception” and not as a RTL expression.
Thing is, in the statement form, "try doing this" means "do this, and you might get an exception, so deal with it". In the 'try default' form, "try this" means "oops, you got an exception, so try this instead". It's using "try" in the opposite way.
default except Exception try expr # breaks L->R evaluation order
Left to right evaluation order is extremely important to me.
I agree with that, RTL evaluation would be pretty odd in Python.
Really? The evaluation order you mention in above didn’t make sense to me until I tried to make sense of it.
The "default except Exception try expr" notation has "try" followed by the thing that might raise an exception, which mimics the statement. The "expr except Exception try default" notation evaluates from left to right. Both make some sense, and I'd say it's on balance which is the more likely to be expected. Imagine an overall expression where it's ambiguous: value = (d["foo"] except KeyError try d["spam"]) Which one do you expect to be tried first? I'd say you could poll a bunch of moderately-experienced Python programmers and get both answers in reasonable numbers. ChrisA
On 27 February 2014 20:44, Ronald Oussoren <ronaldoussoren@mac.com> wrote:
On 27 Feb 2014, at 11:09, Chris Angelico <rosuav@gmail.com> wrote:
On Thu, Feb 27, 2014 at 7:44 PM, Ronald Oussoren <ronaldoussoren@mac.com> wrote:
What about (also mentioned in the PEP)?
value = (expr except Exception try default)
This seems to read nicely, although "try" is at a completely different position than it is in the equivalent try statement.
I like the general idea, but like Brett I don't like using a colon here at all.
I see your "although" clause to be quite a strong objection. In the statement form of an if, you have:
I'm not convinced that this is a strong objection. The order of keywords is different, but that doesn't have to be problem.
As Chris notes, the problem is that if you use "try", the two plausible interpretations are: 1. Evaluates left-to-right, try introduces a different part of the syntax (different from every past statement->expression conversion where the clauses are reordered, but almost always introduced by the same keywords as they are in the statement form) 2. Evaluates right-to-left, try introduces the expressions covered by the exception handler (this is just backwards, and significantly harder to follow than even the "middle first" conditional expression) Neither interpretation is particularly acceptable, and the fact that the other interpretation would remain plausible regardless is a further strike against both of them. Personally, I think the PEP makes a good case for particular semantics with a spelling that isn't great, but isn't completely abhorrent either. I definitely think it represents an improvement over the status quo, which is an ongoing proliferation of function-based special cases for doing particular kinds of exception handling as an expression, and the spelling advocated for in the PEP seems like the best of the (many) alternatives that have been proposed. The way I get the colon in the proposed syntax to make sense to my brain is to view it as being more like the colon in a dictionary key:value pair than it is like the one that introduces a suite or the body of a lambda expression: (lst[2] except {IndexError: "No value"}) The analogy isn't exact (since exception handling is isinstance() based rather than equality based), but I think it gives the right general flavour in terms of the intended meaning of the colon in this construct. The analogy could likely be encouraged and strengthened by changing the parentheses requirements to make this read more like a binary except expression with a parenthesised RHS rather than a ternary expression: lst[2] except (IndexError: "No value") Catching multiple errors would involve a parenthesised tuple as the "key": f() except ((TypeError, AttributeError): "No value") The deferred "multiple except clauses" part of the PEP could also change to be more dict display like: value = expr except ( Exception1: default1, Exception2: default2, Exception3: default3, ) Writing out those examples, I actually like that version (where the parentheses are still mandatory, but the left paren is after the except keyword rather than before the first expression) better than the one currently in the PEP. It also avoids the weirdly unbalanced look of the variant that had the left paren *before* the except keyword. The main downside I see is that the exception handling definition syntax would only be permitted in that specific location, but still look a lot like an ordinary parenthesised expression. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On 2/27/2014 4:57 AM, Nick Coghlan wrote:
The way I get the colon in the proposed syntax to make sense to my brain is to view it as being more like the colon in a dictionary key:value pair than it is like the one that introduces a suite or the body of a lambda expression:
(lst[2] except {IndexError: "No value"})
The analogy isn't exact (since exception handling is isinstance() based rather than equality based), but I think it gives the right general flavour in terms of the intended meaning of the colon in this construct. The analogy could likely be encouraged and strengthened by changing the parentheses requirements to make this read more like a binary except expression with a parenthesised RHS rather than a ternary expression:
lst[2] except (IndexError: "No value")
Catching multiple errors would involve a parenthesised tuple as the "key":
f() except ((TypeError, AttributeError): "No value")
The deferred "multiple except clauses" part of the PEP could also change to be more dict display like:
value = expr except ( Exception1: default1, Exception2: default2, Exception3: default3, )
Writing out those examples, I actually like that version (where the parentheses are still mandatory, but the left paren is after the except keyword rather than before the first expression) better than the one currently in the PEP. It also avoids the weirdly unbalanced look of the variant that had the left paren*before* the except keyword. The main downside I see is that the exception handling definition syntax would only be permitted in that specific location, but still look a lot like an ordinary parenthesised expression.
Cheers, Nick. +1
f() except ((TypeError, AttributeError): "No value") is a nice extension to the idea of value = expr except ( Exception1: default1, Exception2: default2, Exception3: default3, ) Which I've liked since I first saw it, as it neatly solves handling multiple except clauses.
On Fri, Feb 28, 2014 at 6:36 AM, Glenn Linderman <v+python@g.nevcal.com> wrote:
+1
f() except ((TypeError, AttributeError): "No value")
is a nice extension to the idea of
value = expr except ( Exception1: default1, Exception2: default2, Exception3: default3, )
Which I've liked since I first saw it, as it neatly solves handling multiple except clauses.
You can already list multiple exception types. The definition of the exception_list is exactly as per the try/except statement (bar the 'as' keyword, which I would love to see implemented some day). ChrisA
I've added another utility script to my PEP draft repo: https://github.com/Rosuav/ExceptExpr/blob/master/replace_except_expr.py It's built out of some pretty horrendous hacks, it makes some assumptions about code layout (eg spaces for indentation, and a try/except block never has anything else on the same line(s)), and the code's a bit messy, but it does work. I ran it on the Python stdlib and it produced a whole lot of edits (I had it keep the old version in comments, bracketed with markers with the text "PEP 463" in them, which makes it easy to find and analyze); one file (with two try blocks) causes a test failure, but all the rest still pass. If people can try the script on their own codebases and see how it looks, that'd be great. I recommend first running the script with ast.Expr handling removed (as per the file you see above), and then maybe running it with that one line commented out. You'll get a lot of unhelpful change suggestions from the double-expression handler. Could a core committer please apply the last few changes to the PEP? https://raw.github.com/Rosuav/ExceptExpr/master/pep-0463.txt or the diff is below. I think this is the last important change left; things have gone fairly quiet here, and I think the PEP's about ready to request pronouncement, unless someone knows of something I've forgotten. (Have I said "I'll write up a paragraph about that" about anything and not done it yet? Now's the perfect time to remind me.) Thanks! ChrisA diff -r 5f63c8a92d1c pep-0463.txt --- a/pep-0463.txt Mon Feb 24 14:22:46 2014 -0800 +++ b/pep-0463.txt Fri Feb 28 08:25:33 2014 +1100 @@ -43,6 +43,34 @@ * statistics.mean(data) - no way to handle an empty iterator +Had this facility existed early in Python's history, there would have been +no need to create dict.get() and related methods; the one obvious way to +handle an absent key would be to respond to the exception. One method is +written which signal the absence in one way, and one consistent technique +is used to respond to the absence. Instead, we have dict.get(), and as of +Python 3.4, we also have min(... default=default), and myriad others. We +have a LBYL syntax for testing inside an expression, but there is currently +no EAFP notation; compare the following:: + + # LBYL: + if key in dic: + process(dic[key]) + else: + process(None) + # As an expression: + process(dic[key] if key in dic else None) + + # EAFP: + try: + process(dic[key]) + except KeyError: + process(None) + # As an expression: + process(dic[key] except KeyError: None) + +Python generally recommends the EAFP policy, but must then proliferate +utility functions like dic.get(key,None) to enable this. + Rationale ========= @@ -338,6 +366,19 @@ except KeyError: u = tarinfo.uid +Look up an attribute, falling back on a default:: + mode = (f.mode except AttributeError: 'rb') + + # Lib/aifc.py:882: + if hasattr(f, 'mode'): + mode = f.mode + else: + mode = 'rb' + + return (sys._getframe(1) except AttributeError: None) + # Lib/inspect.py:1350: + return sys._getframe(1) if hasattr(sys, "_getframe") else None + Perform some lengthy calculations in EAFP mode, handling division by zero as a sort of sticky NaN:: @@ -616,7 +657,7 @@ 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. -Multiple 'except' keywords can be used, and they will all catch +Multiple 'except' keywords could be used, and they will all catch exceptions raised in the original expression (only):: # Will catch any of the listed exceptions thrown by expr; -- end --
On Fri, Feb 28, 2014 at 8:29 AM, Chris Angelico <rosuav@gmail.com> wrote:
@@ -43,6 +43,34 @@
* statistics.mean(data) - no way to handle an empty iterator
+Had this facility existed early in Python's history, there would have been +no need to create dict.get() and related methods; the one obvious way to +handle an absent key would be to respond to the exception. One method is +written which signal the absence in one way, and one consistent technique
Doh! Typical... I notice the typo only after hitting send. This should be "which signals". The linked-to draft file has been updated. ChrisA
On 28 Feb 2014 05:56, "Chris Angelico" <rosuav@gmail.com> wrote:
On Fri, Feb 28, 2014 at 6:36 AM, Glenn Linderman <v+python@g.nevcal.com>
wrote:
+1
f() except ((TypeError, AttributeError): "No value")
is a nice extension to the idea of
value = expr except ( Exception1: default1, Exception2: default2, Exception3: default3, )
Which I've liked since I first saw it, as it neatly solves handling multiple except clauses.
You can already list multiple exception types. The definition of the exception_list is exactly as per the try/except statement (bar the 'as' keyword, which I would love to see implemented some day).
Note that this example is covering the deferred case of multiple except clauses that resolve to different values, not the already handled case of multiple exception types that resolve to the same value. Anyway, even if you choose not to switch the parenthesis requirement in PEP, it should at least make the "colon as in dict display, not as in suite introduction" comparison, and note this as an alternative proposal for the required parentheses. Cheers, Nick.
ChrisA _______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe:
https://mail.python.org/mailman/options/python-dev/ncoghlan%40gmail.com
On Fri, Feb 28, 2014 at 10:33 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
On 28 Feb 2014 05:56, "Chris Angelico" <rosuav@gmail.com> wrote:
On Fri, Feb 28, 2014 at 6:36 AM, Glenn Linderman <v+python@g.nevcal.com> wrote:
+1
f() except ((TypeError, AttributeError): "No value")
is a nice extension to the idea of
value = expr except ( Exception1: default1, Exception2: default2, Exception3: default3, )
Which I've liked since I first saw it, as it neatly solves handling multiple except clauses.
You can already list multiple exception types. The definition of the exception_list is exactly as per the try/except statement (bar the 'as' keyword, which I would love to see implemented some day).
Note that this example is covering the deferred case of multiple except clauses that resolve to different values, not the already handled case of multiple exception types that resolve to the same value.
The "nice extension" to that notation is already handled, though.
Anyway, even if you choose not to switch the parenthesis requirement in PEP, it should at least make the "colon as in dict display, not as in suite introduction" comparison, and note this as an alternative proposal for the required parentheses.
Hmm. Not sure that it really helps, but okay. Will word something up. ChrisA
On 2/27/2014 3:40 PM, Chris Angelico wrote:
On Fri, Feb 28, 2014 at 10:33 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
On 28 Feb 2014 05:56, "Chris Angelico" <rosuav@gmail.com> wrote:
On Fri, Feb 28, 2014 at 6:36 AM, Glenn Linderman <v+python@g.nevcal.com> wrote:
+1
f() except ((TypeError, AttributeError): "No value")
is a nice extension to the idea of
value = expr except ( Exception1: default1, Exception2: default2, Exception3: default3, )
Which I've liked since I first saw it, as it neatly solves handling multiple except clauses. You can already list multiple exception types. The definition of the exception_list is exactly as per the try/except statement (bar the 'as' keyword, which I would love to see implemented some day). Note that this example is covering the deferred case of multiple except clauses that resolve to different values, not the already handled case of multiple exception types that resolve to the same value. The "nice extension" to that notation is already handled, though.
Yes. But the point is really the location of the (), sorry if my "nice extension" comment is throwing you off that track. value = expr except ( Exception1: default1, Exception2: default2, Exception3: default3, )
Anyway, even if you choose not to switch the parenthesis requirement in PEP, it should at least make the "colon as in dict display, not as in suite introduction" comparison, and note this as an alternative proposal for the required parentheses. Hmm. Not sure that it really helps, but okay. Will word something up.
ChrisA _______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/v%2Bpython%40g.nevcal.com
On Fri, Feb 28, 2014 at 1:12 PM, Glenn Linderman <v+python@g.nevcal.com> wrote:
Yes. But the point is really the location of the (), sorry if my "nice extension" comment is throwing you off that track.
Ah! I see. We touched on this syntax on -ideas, but at the time, the proposed syntax didn't have parens around the outside. Either location will solve most of the same problems (like precedence/associativity versus multiple except clauses), but shrinking down to just the exception list and default value makes it look very different. It looks like what's inside should be an expression, which it isn't. What's the advantage of this form? What's its key advantage over the parens-around-the-whole-thing form? ChrisA
On 2/27/2014 7:46 PM, Chris Angelico wrote:
On Fri, Feb 28, 2014 at 1:12 PM, Glenn Linderman <v+python@g.nevcal.com> wrote:
Yes. But the point is really the location of the (), sorry if my "nice extension" comment is throwing you off that track. Ah! I see.
We touched on this syntax on -ideas, but at the time, the proposed syntax didn't have parens around the outside. Either location will solve most of the same problems (like precedence/associativity versus multiple except clauses), but shrinking down to just the exception list and default value makes it look very different. It looks like what's inside should be an expression, which it isn't. What's the advantage of this form? What's its key advantage over the parens-around-the-whole-thing form?
Key advantage to me is that if a function call or other expression may produce multiple exceptions, the syntax doesn't require repeating the "except" over and over. By not repeating the except over and over, there is less ambiguity about what expression the Exception-lists apply to, when there is more than one Exception list in the expression. Whereas the current PEP syntax has ambiguity regarding how to interpret a-expr except except-list-b: b-expr except except-list-c: c-expr (does the 2nd except apply to a-expr or b-expr?), without parentheses, and, as far as I am concerned, even with the parentheses, this syntax makes it very clear that each of the Exception-lists apply to a-expr. Key advantage to others may be that because the : is within the () [and the leading ( is quite nearby, making it obvious], it is less likely to be considered a statement boundary, and more easily explained as a special type of list syntax... not _really_ a list, because it is really code to be executed somewhat sequentially rather than data, and lists don't have : ... and not _really_ a dict constant, which does have :, because the Exception is not _really_ a key, but the syntax can draw on analogies with the dict constant syntax which will help people remember it, and even sort of understand that there is a pair-wise relationship between the Exception-list and the expression after the :, without repeating the except over and over.
On Fri, Feb 28, 2014 at 6:38 PM, Glenn Linderman <v+python@g.nevcal.com> wrote:
Whereas the current PEP syntax has ambiguity regarding how to interpret a-expr except except-list-b: b-expr except except-list-c: c-expr (does the 2nd except apply to a-expr or b-expr?), without parentheses, and, as far as I am concerned, even with the parentheses, this syntax makes it very clear that each of the Exception-lists apply to a-expr.
Fair enough. It's a bit hard to talk about multiple except expressions, though, as they're a problem unless formally supported - and they're almost never needed. Really, all you need to do is say "never abut except-expressions without parentheses" (which the current proposal and the "parens around the exception bit only" proposal both enforce), and then there's no problem. I expect that normal use of this won't include any form of chaining. Yes, it can - like any feature - be used abnormally, but at some point it's better to just drop it out as a statement.
Key advantage to others may be that because the : is within the () [and the leading ( is quite nearby, making it obvious], it is less likely to be considered a statement boundary, and more easily explained as a special type of list syntax... not _really_ a list, because it is really code to be executed somewhat sequentially rather than data, and lists don't have : ... and not _really_ a dict constant, which does have :, because the Exception is not _really_ a key, but the syntax can draw on analogies with the dict constant syntax which will help people remember it, and even sort of understand that there is a pair-wise relationship between the Exception-list and the expression after the :, without repeating the except over and over.
See the confusing terminology we have here? It might be called a "list" of except-expressions, but the brackets are round, and a list's are square. It's kinda like dict syntax, only again, the other sort of bracket, and it's wrong to try to construct a dict; it'd be too tempting to conflate this with some of the other current proposals for lazily-evaluated expressions (aka "simpler syntax for lambda" or other terms). This is, fundamentally, a multi-part expression on par with "and" and "or": first, evaluate the primary expression; then, if an exception is raised, evaluate the exception list and see if it matches; then, if it matches, squash the exception and evaluate the default expression. You can't turn that into a dict, partly because you'd need to sort out lazy evaluation, and partly because a dict is unordered - if this is expanded to support multiple except clauses, they have to be processed in order. (You might, for instance, catch ZeroDivisionError, and then Exception, with different handling. It'd be VERY confusing for them to be processed in the wrong order, particularly if it happens unpredictably.) Are there any other expressions that allow parens around a part of the expression, without the stuff inside them becoming a completely separate sub-expression? ChrisA
On 2/28/2014 12:41 AM, Chris Angelico wrote:
Whereas the current PEP syntax has ambiguity regarding how to interpret a-expr except except-list-b: b-expr except except-list-c: c-expr (does the 2nd except apply to a-expr or b-expr?), without parentheses, and, as far as I am concerned, even with the parentheses, this syntax makes it very clear that each of the Exception-lists apply to a-expr. Fair enough. It's a bit hard to talk about multiple except expressions, though, as they're a problem unless formally supported - and they're almost never needed. Really, all you need to do is say "never abut except-expressions without parentheses" (which the current
On Fri, Feb 28, 2014 at 6:38 PM, Glenn Linderman <v+python@g.nevcal.com> wrote: proposal and the "parens around the exception bit only" proposal both enforce), and then there's no problem. I expect that normal use of this won't include any form of chaining. Yes, it can - like any feature - be used abnormally, but at some point it's better to just drop it out as a statement.
Key advantage to others may be that because the : is within the () [and the leading ( is quite nearby, making it obvious], it is less likely to be considered a statement boundary, and more easily explained as a special type of list syntax... not _really_ a list, because it is really code to be executed somewhat sequentially rather than data, and lists don't have : ... and not _really_ a dict constant, which does have :, because the Exception is not _really_ a key, but the syntax can draw on analogies with the dict constant syntax which will help people remember it, and even sort of understand that there is a pair-wise relationship between the Exception-list and the expression after the :, without repeating the except over and over. See the confusing terminology we have here? It might be called a "list" of except-expressions, but the brackets are round, and a list's are square. It's kinda like dict syntax, only again, the other sort of bracket, and it's wrong to try to construct a dict; it'd be too tempting to conflate this with some of the other current proposals for lazily-evaluated expressions (aka "simpler syntax for lambda" or other terms). This is, fundamentally, a multi-part expression on par with "and" and "or": first, evaluate the primary expression; then, if an exception is raised, evaluate the exception list and see if it matches; then, if it matches, squash the exception and evaluate the default expression. You can't turn that into a dict, partly because you'd need to sort out lazy evaluation, and partly because a dict is unordered - if this is expanded to support multiple except clauses, they have to be processed in order. (You might, for instance, catch ZeroDivisionError, and then Exception, with different handling. It'd be VERY confusing for them to be processed in the wrong order, particularly if it happens unpredictably.)
That's why I kept saying "not _really_" :) It isn't a list, but it has an order requirement; it isn't a dict, but it has pairs; and finally, it isn't data, but code. But nonetheless the analogies are somewhat useful.
Are there any other expressions that allow parens around a part of the expression, without the stuff inside them becoming a completely separate sub-expression?
Sure. Function invocation. You can claim (and it is accurate) that the stuff inside is somewhat independent of the actual function called, but the syntax is function-name open-paren parameter-list close-paren, and the stuff in the parens would be a tuple if it were purely data, except not quite a tuple because some items are pairs (name and value), but it winds up being neither a tuple, nor a list, nor a dict, but instead a complex structure related to code execution :) Actually, this sounds quite similar to value = expr except ( Exception1: default1, Exception2: default2, Exception3: default3, ) except that to get the pairing aspect of some parameters for a function call, you use = instead of :, and instead of a named function it has an expression and a keyword. Not being an expert parser generator, I don't know if the () could be made optional if there is only one exception-list, but that would also remove one of the benefits some folks might perceive with using this syntax, and would also make the analogy with function call syntax a little less comparable. Glenn
On Fri, Feb 28, 2014 at 1:30 AM, Glenn Linderman <v+python@g.nevcal.com>wrote:
value = expr except ( Exception1: default1, Exception2: default2, Exception3: default3, )
except that to get the pairing aspect of some parameters for a function call, you use = instead of :, and instead of a named function it has an expression and a keyword.
[ Cue people suggesting the use of '=' or '=>' or '->' instead of ':' ]
Not being an expert parser generator, I don't know if the () could be made optional if there is only one exception-list, but that would also remove one of the benefits some folks might perceive with using this syntax, and would also make the analogy with function call syntax a little less comparable.
I believe it's possible, but it's probably tricky: the parser has to consider two or three cases:
1. expr1 except expr2: expr4 2. expr1 except (expr2: expr4) 3. expr1 except (expr2, expr3): expr4 4. expr1 except ((expr2, expr3): expr4) (for simplicity I use 'expr' here, which is entirely the wrong thing to do in terms of CPython's grammar: there's different types of expressions for different situations. #2 and #4 are actually automatically derived from #1 and #3 by 'expr' including tuples and parenthesized expressions.) CPython's parser is a LL(1) parser, which means it can look ahead 1 character to choose between alternatives (and that's not going to change, Guido likes it this way -- and so I do, personally :). Looking at #2 and #3 you can't tell which alternative to use until you see the ':' or ',' after 'expr2', which is too late. The only way to handle that, I think, is to include all possible alternatives in the grammar for the 'except' statement, duplicating logic and definitions from a lot of places. We already do this kind of thing to deal with other potentially-ambiguous situations (to handle dicts and sets and dict comprehensions and set comprehensions, for example) but this would be even more duplication. It would be easier if we weren't talking about '(' or any other token that can be part of a normal expression ;-P [ Cue people suggesting the use of 'expr1 except < expr2: expr4 >'... ] -- Thomas Wouters <thomas@python.org> Hi! I'm an email virus! Think twice before sending your email to help me spread!
On Fri, Feb 28, 2014 at 8:30 PM, Glenn Linderman <v+python@g.nevcal.com> wrote:
Are there any other expressions that allow parens around a part of the expression, without the stuff inside them becoming a completely separate sub-expression?
Sure. Function invocation. You can claim (and it is accurate) that the stuff inside is somewhat independent of the actual function called, but the syntax is function-name open-paren parameter-list close-paren, and the stuff in the parens would be a tuple if it were purely data, except not quite a tuple because some items are pairs (name and value), but it winds up being neither a tuple, nor a list, nor a dict, but instead a complex structure related to code execution :)
Thanks, that's exactly the sort of example I was looking for :) For some reason I mentally blanked and didn't think of that. My point is, if you're going to look for parens around the bit after except, we should look at styling and layout that follows how that's done. As TW says:
[ Cue people suggesting the use of '=' or '=>' or '->' instead of ':' ]
If this is to be laid out like a function call, '=' would indeed make sense. But let's stick to the colon for now, so this is a mix of dict-style and function-call-style. expr except(Exception: default) In a more function-call-style layout, that would be: expr except(Exception=default) Both of those look reasonable, and they're tying the parens to the word "except" so it can't *actually* be a function call. Then it looks like it should be laid out like this: except(expr, Exception=default) and it really *is* looking like a function call - and now it's hit the uncanny valley [1], where it's close enough to be a problem (eg because of lazy evaluation, or because "Exception" could be "(AttributeError, KeyError)", which can't be a keyword argument). I don't like this last form; does anyone else support it? Removing the space after the word "except" makes me a lot less averse to this form. Funny how stylistic choices can influence a structural one! Here are a few more examples. How do other people feel about them? cond = args[1] except(IndexError: None) pwd = os.getcwd() except(OSError: None) e.widget = self._nametowidget(W) except(KeyError=W) line = readline() except(StopIteration='') _CONFIG_VARS['abiflags'] = sys.abiflags except(AttributeError: '') def getNamedItem(self, name): return self._attrs[name] except(KeyError: None) g = grp.getgrnam(tarinfo.gname)[2] except(KeyError=tarinfo.gid) u = pwd.getpwnam(tarinfo.uname)[2] except(KeyError=tarinfo.uid) mode = f.mode except(AttributeError: 'rb') return sys._getframe(1) except(AttributeError=None) ips.append(ip.ip except(AttributeError: ip.network_address)) dirlist.append(_os.getcwd() except((AttributeError, OSError)=_os.curdir)) The last one is really critical here, I think. It's the only stdlib example I could find that catches two different errors (IIRC). Here it is with the colon: dirlist.append(_os.getcwd() except((AttributeError, OSError): _os.curdir)) Thoughts? ChrisA [1] http://tvtropes.org/pmwiki/pmwiki.php/Main/UncannyValley
On 28 February 2014 21:46, Chris Angelico <rosuav@gmail.com> wrote:
On Fri, Feb 28, 2014 at 8:30 PM, Glenn Linderman <v+python@g.nevcal.com> wrote:
Are there any other expressions that allow parens around a part of the expression, without the stuff inside them becoming a completely separate sub-expression?
Sure. Function invocation. You can claim (and it is accurate) that the stuff inside is somewhat independent of the actual function called, but the syntax is function-name open-paren parameter-list close-paren, and the stuff in the parens would be a tuple if it were purely data, except not quite a tuple because some items are pairs (name and value), but it winds up being neither a tuple, nor a list, nor a dict, but instead a complex structure related to code execution :)
Thanks, that's exactly the sort of example I was looking for :) For some reason I mentally blanked and didn't think of that. My point is, if you're going to look for parens around the bit after except, we should look at styling and layout that follows how that's done.
Also generator expressions and most uses of yield or yield from as embedded expressions. Parentheses are our general "this next bit may not be following the normal syntax rules" utility, in addition to being used to override the normal precedence rules (square brackets and curly braces similarly denote regions where the parsing rules may differ from the top level ones). As far as the specific problem I'm attacking with this variant of the proposal goes, one of the recurring reactions from different people has been "but colons are used to introduce suites, not expressions". This impression is not, in fact, correct, as the colon is already used as a subexpression separator in four cases: * dict display key:value pairs * slice notation start:stop:step triples * function parameter : annotation separation * lambda expression arg list : return value separation PEP 463 just adds a fifth such case, as an exception spec:result separator. The preferred notation in the PEP most resembles the existing lambda use case, with "except" instead of "lambda", an exception handling spec instead of an argument list and an additional leading expression: (expr except Exception: default) Lots of people don't like the lambda notation though, so it isn't necessarily a particularly compelling parallel to use. By contrast, it's rare to hear any objections to the {key:value} dict display syntax. Hence the proposed tweak to the syntax to define an "exception handler expression" syntax that is analogous to a dict display rather than a lambda expression: expr except (Exception: default) However, I have realised that there *is* a major downside to that notation, which is that it lacks the connotations of lazy evaluation associated with lambda expressions, whereas the default result of an except expression won't be evaluated at all if the exception isn't thrown. So I think that on balance, I actually do prefer your current proposal. That said, I do think this is a variant worth discussing explicitly in the PEP, if only to remind people that there's definitely precedent for using a colon to separate two subexpressions inside a larger expression element - it's not *just* used to introduce suites, even though that is by far the most *common* use case. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Fri, Feb 28, 2014 at 11:51 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
So I think that on balance, I actually do prefer your current proposal. That said, I do think this is a variant worth discussing explicitly in the PEP, if only to remind people that there's definitely precedent for using a colon to separate two subexpressions inside a larger expression element - it's not *just* used to introduce suites, even though that is by far the most *common* use case.
I've added a bit more to the PEP about that. https://github.com/Rosuav/ExceptExpr/commit/f32387 Does that explain it, do you think? ChrisA
On 28 February 2014 23:07, Chris Angelico <rosuav@gmail.com> wrote:
On Fri, Feb 28, 2014 at 11:51 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
So I think that on balance, I actually do prefer your current proposal. That said, I do think this is a variant worth discussing explicitly in the PEP, if only to remind people that there's definitely precedent for using a colon to separate two subexpressions inside a larger expression element - it's not *just* used to introduce suites, even though that is by far the most *common* use case.
I've added a bit more to the PEP about that.
https://github.com/Rosuav/ExceptExpr/commit/f32387
Does that explain it, do you think?
Yeah, that works. You may also want to add a "common objections" section to explicitly cover the "but colons introduce suites" objection. That would provide a space to explicitly list all of the current "not introducing a suite" use cases for the colon in Python's syntax, since increasing that tally from four to five is less radical than introducing the first non-suite related use case for the symbol. Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia
On Fri, Feb 28, 2014 at 11:51 PM, Nick Coghlan <ncoghlan@gmail.com> wrote:
Are there any other expressions that allow parens around a part of the expression, without the stuff inside them becoming a completely separate sub-expression?
Also generator expressions and most uses of yield or yield from as embedded expressions. Parentheses are our general "this next bit may not be following the normal syntax rules" utility, in addition to being used to override the normal precedence rules (square brackets and curly braces similarly denote regions where the parsing rules may differ from the top level ones).
In those cases, the "stuff inside the parens" is the entire syntactic structure of that sub-element, right? I was looking for something where there's syntax inside and syntax outside the parens, where what's in the parens isn't just an expression or tuple, so I could try laying out an except expression to imitate that style. The function call is excellent; removing one space from the layout did make the expression look better, but as explained above, I still prefer the current form. Would like to make the parens optional; maybe make them mandatory if there are two except-expressions abutting, or something, but I'd like to simplify the syntax for the common case. Otherwise, current syntax is still my personal preferred. Of course, we still have to convince Guido that this is a good idea at all, syntax or no :) ChrisA
On Sat, Mar 1, 2014 at 12:24 AM, Nick Coghlan <ncoghlan@gmail.com> wrote:
Yeah, that works. You may also want to add a "common objections" section to explicitly cover the "but colons introduce suites" objection. That would provide a space to explicitly list all of the current "not introducing a suite" use cases for the colon in Python's syntax, since increasing that tally from four to five is less radical than introducing the first non-suite related use case for the symbol.
https://github.com/Rosuav/ExceptExpr/commit/b770f9 Any other objections that should be listed? ChrisA
On 2/28/2014 4:51 AM, Nick Coghlan wrote:
The preferred notation in the PEP most resembles the existing lambda use case, with "except" instead of "lambda", an exception handling spec instead of an argument list and an additional leading expression:
(expr except Exception: default)
Lots of people don't like the lambda notation though, so it isn't necessarily a particularly compelling parallel to use.
By contrast, it's rare to hear any objections to the {key:value} dict display syntax. Hence the proposed tweak to the syntax to define an "exception handler expression" syntax that is analogous to a dict display rather than a lambda expression:
expr except (Exception: default)
However, I have realised that there*is* a major downside to that notation, which is that it lacks the connotations of lazy evaluation associated with lambda expressions, whereas the default result of an except expression won't be evaluated at all if the exception isn't thrown. You are overlooking that the keyword except provides exactly the connotation of lazy evaluation, so if this is your only reason for
Thank you for explaining why I find the above notation awkward. ": as introducing a suite" never bothered me, because, as you've now enumerated, there are other uses of :. But the lambda syntax parallel is what I don't like about it... I find the lambda syntax hard to read. preferring the lambda syntax, you just erased it :)
On Sat, Mar 1, 2014 at 12:24 PM, Glenn Linderman <v+python@g.nevcal.com> wrote:
You are overlooking that the keyword except provides exactly the connotation of lazy evaluation, so if this is your only reason for preferring the lambda syntax, you just erased it :)
Statements are always executed sequentially. That's not "lazy evaluation", that's just the way statements are :) ChrisA
On Thu, Feb 27, 2014 at 1:29 PM, Chris Angelico <rosuav@gmail.com> wrote:
+Had this facility existed early in Python's history, there would have been +no need to create dict.get() and related methods;
FWIW, after experimenting and some consideration I've come to the conclusion that this is incorrect. 'd[k] except KeyError: default' is still much broader than dict.get(k): Python 3.4.0rc1+ (default:aa2ae744e701+, Feb 24 2014, 01:22:15) [GCC 4.6.3] on linux Type "help", "copyright", "credits" or "license" for more information.
expensive_calculation = hash class C: ... _hash_cache = {} ... def __init__(self, value): ... self.value = value ... if value not in self._hash_cache: ... self._hash_cache[value] = expensive_calculation(value) ... def __hash__(self): ... return self._hash_cache[self.value] ... def __eq__(self, other): ... return self.value == other ... a, b, c, d = C(1), C(2), C(3), C(4) D = {a: 1, b: 2, c: 3, d: 4}
a.value = 5 print("except expr:", (D[a] except KeyError: 'default')) except expr: default print("dict.get:", D.get(a, 'default')) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 8, in __hash__ KeyError: 5
All in all I believe I will continue to prefer specific methods for specific use-cases; I'm -0 on the idea of an except-expression, -0 on the syntax with the mandatory parentheses around the whole thing (and so far -1 on any of the other suggested forms.) I can see the attractiveness, but frankly, all the suggested changes to the stdlib fall in two categories: easier to express (and narrower in its exception handling) with e.g. dict.get for the trivial ones, or much better written out using temporary variables for the complex ones. As soon as an except-expression has trouble fitting on two lines it becomes an unsightly mess; it no longer becomes obvious what it does from a glance. Not having except-expr may mean we keep adding methods (with different names, and slightly different semantics) to cover specific use-cases of specific types, but I can live with that. -- Thomas Wouters <thomas@python.org> Hi! I'm an email virus! Think twice before sending your email to help me spread!
On Thu, Mar 6, 2014 at 7:57 AM, Thomas Wouters <thomas@python.org> wrote:
On Thu, Feb 27, 2014 at 1:29 PM, Chris Angelico <rosuav@gmail.com> wrote:
+Had this facility existed early in Python's history, there would have been +no need to create dict.get() and related methods;
FWIW, after experimenting and some consideration I've come to the conclusion that this is incorrect. 'd[k] except KeyError: default' is still much broader than dict.get(k):
*Much* broader? You prove that it's broader, yes, but most types aren't defining __hash__ methods that can fail with KeyError. Can you show any real-world code that trips this? I'd say the broadened exception scope (or, putting it the other way, the narrowed exception scope of adding dict.get() after except-expressions had already existed) is insufficiently significant to justify adding an extra method to the dict. Since the method does exist, it will continue to be useful, but if except expressions did and dict.get() didn't, there'd have been very little justification for them. And certainly hasattr() wouldn't need to exist, since it exactly _does_ try to get the attribute and see if AttributeError is raised. ChrisA
On Thu, Mar 6, 2014 at 7:57 AM, Thomas Wouters <thomas@python.org> wrote:
All in all I believe I will continue to prefer specific methods for specific use-cases; I'm -0 on the idea of an except-expression, -0 on the syntax with the mandatory parentheses around the whole thing (and so far -1 on any of the other suggested forms.) I can see the attractiveness, but frankly, all the suggested changes to the stdlib fall in two categories: easier to express (and narrower in its exception handling) with e.g. dict.get for the trivial ones, or much better written out using temporary variables for the complex ones. As soon as an except-expression has trouble fitting on two lines it becomes an unsightly mess; it no longer becomes obvious what it does from a glance. Not having except-expr may mean we keep adding methods (with different names, and slightly different semantics) to cover specific use-cases of specific types, but I can live with that.
Most of those concerns could also be aimed at the if/else expression. There are definitely places that do not merit its use, but that doesn't mean the feature is a bad one. The PEP has a number of examples that fit quite happily on a single line, and it's those that I'm advocating. We have comprehensions, genexps, etc, etc, all (or at least most) of which can be written out in some form of long-hand, and it's usually better to use the short-hand - it's not just laziness, it's expressiveness. Speaking of the PEP, if someone could apply the latest changes, I think it's pretty much ready for pronouncement. Thanks! Content: https://raw.github.com/Rosuav/ExceptExpr/master/pep-0463.txt Diff from current peps repo: diff -r 2cf89e9e50a3 pep-0463.txt --- a/pep-0463.txt Tue Mar 04 18:47:44 2014 -0800 +++ b/pep-0463.txt Thu Mar 06 11:12:44 2014 +1100 @@ -250,7 +250,8 @@ 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 +"keyword name_list: subexpression"; it also can be read as mapping Exception +to the default value, dict-style. 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" @@ -271,6 +272,18 @@ Using the preferred order, subexpressions will always be evaluated from left to right, no matter how the syntax is nested. +Keeping the existing notation, but shifting the mandatory parentheses, we +have the following suggestion:: + + value = expr except (Exception: default) + value = expr except(Exception: default) + +This is reminiscent of a function call, or a dict initializer. The colon +cannot be confused with introducing a suite, but on the other hand, the new +syntax guarantees lazy evaluation, which a dict does not. The potential +to reduce confusion is considered unjustified by the corresponding potential +to increase it. + Example usage ============= @@ -854,6 +867,32 @@ expression to achieve this. +Common objections +================= + +Colons always introduce suites +------------------------------ + +While it is true that many of Python's syntactic elements use the colon to +introduce a statement suite (if, while, with, for, etcetera), this is not +by any means the sole use of the colon. Currently, Python syntax includes +four cases where a colon introduces a subexpression: + +* dict display - { ... key:value ... } +* slice notation - [start:stop:step] +* function definition - parameter : annotation +* lambda - arg list: return value + +This proposal simply adds a fifth: + +* except-expression - exception list: result + +Style guides and PEP 8 should recommend not having the colon at the end of +a wrapped line, which could potentially look like the introduction of a +suite, but instead advocate wrapping before the exception list, keeping the +colon clearly between two expressions. + + Copyright ========= ChrisA
On Wed, Mar 5, 2014 at 4:28 PM, Chris Angelico <rosuav@gmail.com> wrote:
All in all I believe I will continue to prefer specific methods for specific use-cases; I'm -0 on the idea of an except-expression, -0 on the syntax with the mandatory parentheses around the whole thing (and so far -1 on any of the other suggested forms.) I can see the attractiveness, but frankly, all the suggested changes to the stdlib fall in two categories: easier to express (and narrower in its exception handling) with e.g. dict.get for
trivial ones, or much better written out using temporary variables for
On Thu, Mar 6, 2014 at 7:57 AM, Thomas Wouters <thomas@python.org> wrote: the the
complex ones. As soon as an except-expression has trouble fitting on two lines it becomes an unsightly mess; it no longer becomes obvious what it does from a glance. Not having except-expr may mean we keep adding methods (with different names, and slightly different semantics) to cover specific use-cases of specific types, but I can live with that.
Most of those concerns could also be aimed at the if/else expression.
And I did :) But the if-else expression had a single thing going for it, the thing that landed the actual feature: it prevents people from using buggy alternatives in the quest for short code, the and-or trick. You may remember that Guido initially rejected the if-else expression, until he realized people were going to use something like it whether he liked it or not. The except-expression has a different issue: people catching exceptions too broadly. I don't believe that's as big a problem, nor is it as subtly wrong. And the except-expression doesn't solve _all_ such issues, just a very small subset. It's just another thing to learn when you're new, and just another thing to consider when reviewing code.
There are definitely places that do not merit its use, but that doesn't mean the feature is a bad one. The PEP has a number of examples that fit quite happily on a single line, and it's those that I'm advocating. We have comprehensions, genexps, etc, etc, all (or at least most) of which can be written out in some form of long-hand, and it's usually better to use the short-hand - it's not just laziness, it's expressiveness.
It's not a question of it being expressive, it's a question of it being worth separate syntax. I don't think it's worth syntax.
Speaking of the PEP, if someone could apply the latest changes, I think it's pretty much ready for pronouncement. Thanks!
PEP update pushed (changeset 59653081cdf6.)
Content: https://raw.github.com/Rosuav/ExceptExpr/master/pep-0463.txt
Diff from current peps repo:
diff -r 2cf89e9e50a3 pep-0463.txt --- a/pep-0463.txt Tue Mar 04 18:47:44 2014 -0800 +++ b/pep-0463.txt Thu Mar 06 11:12:44 2014 +1100 @@ -250,7 +250,8 @@ 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 +"keyword name_list: subexpression"; it also can be read as mapping Exception +to the default value, dict-style. 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" @@ -271,6 +272,18 @@ Using the preferred order, subexpressions will always be evaluated from left to right, no matter how the syntax is nested.
+Keeping the existing notation, but shifting the mandatory parentheses, we +have the following suggestion:: + + value = expr except (Exception: default) + value = expr except(Exception: default) + +This is reminiscent of a function call, or a dict initializer. The colon +cannot be confused with introducing a suite, but on the other hand, the new +syntax guarantees lazy evaluation, which a dict does not. The potential +to reduce confusion is considered unjustified by the corresponding potential +to increase it. +
Example usage ============= @@ -854,6 +867,32 @@ expression to achieve this.
+Common objections +================= + +Colons always introduce suites +------------------------------ + +While it is true that many of Python's syntactic elements use the colon to +introduce a statement suite (if, while, with, for, etcetera), this is not +by any means the sole use of the colon. Currently, Python syntax includes +four cases where a colon introduces a subexpression: + +* dict display - { ... key:value ... } +* slice notation - [start:stop:step] +* function definition - parameter : annotation +* lambda - arg list: return value + +This proposal simply adds a fifth: + +* except-expression - exception list: result + +Style guides and PEP 8 should recommend not having the colon at the end of +a wrapped line, which could potentially look like the introduction of a +suite, but instead advocate wrapping before the exception list, keeping the +colon clearly between two expressions. + + Copyright =========
ChrisA _______________________________________________ Python-Dev mailing list Python-Dev@python.org https://mail.python.org/mailman/listinfo/python-dev Unsubscribe: https://mail.python.org/mailman/options/python-dev/thomas%40python.org
-- Thomas Wouters <thomas@python.org> Hi! I'm an email virus! Think twice before sending your email to help me spread!
On Wed, Mar 05, 2014 at 12:57:03PM -0800, Thomas Wouters wrote:
On Thu, Feb 27, 2014 at 1:29 PM, Chris Angelico <rosuav@gmail.com> wrote:
+Had this facility existed early in Python's history, there would have been +no need to create dict.get() and related methods;
FWIW, after experimenting and some consideration I've come to the conclusion that this is incorrect. 'd[k] except KeyError: default' is still much broader than dict.get(k):
I don't think your example proves what you think it does. I think it demonstrates a bug in the dict.get method. The documentation for get states clearly that get will never raise KeyError: Return the value for key if key is in the dictionary, else default. If default is not given, it defaults to None, so that this method never raises a KeyError. http://docs.python.org/3/library/stdtypes.html#dict.get but your example demonstrates that in fact it can raise KeyError (albeit under some rather unusual circumstances):
Python 3.4.0rc1+ (default:aa2ae744e701+, Feb 24 2014, 01:22:15) [GCC 4.6.3] on linux Type "help", "copyright", "credits" or "license" for more information.
expensive_calculation = hash class C: ... _hash_cache = {} ... def __init__(self, value): ... self.value = value ... if value not in self._hash_cache: ... self._hash_cache[value] = expensive_calculation(value) ... def __hash__(self): ... return self._hash_cache[self.value] ... def __eq__(self, other): ... return self.value == other ... a, b, c, d = C(1), C(2), C(3), C(4) D = {a: 1, b: 2, c: 3, d: 4} a.value = 5
print("except expr:",