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 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 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 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
[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 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 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 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 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 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
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
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 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
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)
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 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 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
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.
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.
"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 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 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
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 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 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, 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)
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 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 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
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, 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, 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
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 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!
The PEP currently says:
Alternative Proposals =====================
Discussion on python-ideas brought up the following syntax suggestions::
value = expr except default if Exception [as e]
This one was rejected because of the out-of-order evaluation. Note, however, that the (farthest left) expr is always evaluated first; the only out-of-order evaluation is "default if Exception". "default if Exception" is precisely the same evaluation order (clause after the "if" skips ahead of the clause before the "if") as in the existing if-expression, and the existing if-filters in comprehensions. The same justifications for that order violation generally apply here too. You can argue that they weren't sufficient justification in the first place, but that is water under the bridge; *re*-using out-of-order-"if" shouldn't add any additional costs. [Err... unless people take the "if" too literally, and treat the Exception clause as a boolean value, instead of as an argument to the "except" keyword.] The advantages of this form get much stronger with [as e] or multiple different except clauses, but some of them do apply to even the simplest form. Notably, the "say it like you would in English" that convinced Perl still applies: "if" *without* a "then" is normally an extra condition added after the main point: Normally ham, but fish if it's a Friday. (Admittedly, the word "then" *can* be elided (and represented by a slight pause), and python programmers are already used to seeing it represented only by ":\n") I also give a fair amount of weight to the fact that this form starts to look awkward at pretty much the same time the logic gets too complicated for an expression -- that should discourage abuse. [The analogies to if-expressions and if-filters and to spoken English, along with discouragement for abuse, make this my preferred form.] ...
value = expr except (Exception [as e]: default)
(and the similar but unmentioned) value = expr except (Exception [as e] -> default) The mapping analogy for ":" is good -- and is the reason to place parentheses there, as opposed to around the whole expression. Your preferred form -- without the internal parentheses -- looks very much like a suite-introduction, and not at all like the uses where an inline colon is acceptable. I do understand your concern that the parentheses make "except (...)" look too much like a function call -- but I'm not sure that is all bad, let alone as bad as looking like a suite introduction. Both ":" and "->" are defined for signatures; the signature meaning of ":" is tolerable, and the signature meaning of "->" is good. ...
value = expr except Exception [as e] continue with default
This one works for me, but only because I read "continue with" as a compound keyword. I assume the parser would too. :D But I do recognize that it is a poor choice for those who see the space as a more solid barrier. ...
value = expr except(Exception) default # Catches only the named type(s)
This looks too much like the pre-"as" way of capturing an exception.
value = default if expr raise Exception
(Without new keyword "raises",) I would have to work really hard not to read that as: __temp = default if expr: raise Exception value = __temp
value = expr or else default if Exception
To me, this just seems like a wordier and more awkward version of expr except (default if Exception [as e]) including the implicit parentheses around "default if Exception".
value = expr except Exception [as e] -> default
Without parens to group Exception and default, this looks too much like an annotation describing what the expr should return. value = expr except Exception [as e] pass default I would assume that this skipped the statement, like an if-filter in a comprehension.
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.
Nick is right that you should specify whether it is deferred or rejected, because the simplest implementation may lock you into too broad a scope if it is added later.
The four forms most supported by this proposal are, in order::
value = (expr except Exception: default) value = (expr except Exception -> default)
... If there are not parentheses after "except", it will be very tempting (and arguably correct) to (at least mentally) insert them around the first two clauses -- which are evaluated first. But that leaks into value = (expr except Exception): default which strongly resembles the suite-starter ":", but has very little in common with the mapping ":" or the signature ":". value = (expr except Exception) -> default which looks like an annotation, rather than a part of the value-determination. -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 Fri, Mar 7, 2014 at 7:29 AM, Jim J. Jewett <jimjjewett@gmail.com> wrote:
The PEP currently says:
Alternative Proposals =====================
Discussion on python-ideas brought up the following syntax suggestions::
value = expr except default if Exception [as e]
This one was rejected because of the out-of-order evaluation.
Note, however, that the (farthest left) expr is always evaluated first; the only out-of-order evaluation is "default if Exception".
"default if Exception" is precisely the same evaluation order (clause after the "if" skips ahead of the clause before the "if") as in the existing if-expression, and the existing if-filters in comprehensions.
Yes, but that's still out of order.
The same justifications for that order violation generally apply here too. You can argue that they weren't sufficient justification in the first place, but that is water under the bridge; *re*-using out-of-order-"if" shouldn't add any additional costs.
[Err... unless people take the "if" too literally, and treat the Exception clause as a boolean value, instead of as an argument to the "except" keyword.]
The other thing to note is that it's somewhat ambiguous. Until you find that there isn't an else clause, it could be the equally valid "expr except (default if cond else other_default)", with the actual "if Exception" part still to come. (Style guides should, of course, decry this as unreadable, but both the machine parser and any humans reading the code have to assume style guides mightn't be followed.)
The advantages of this form get much stronger with [as e] or multiple different except clauses, but some of them do apply to even the simplest form.
Multiple different except clauses would make for an even messier evaluation order: expr1 except expr3 if expr2 except expr5 if expr4 If you consider the exception type to be the condition, then this makes sense (that is, if you read it as "if isinstance(thrown_exception, Exception)"); but the most obvious reading of "if", both in its statement form and as part of "true if expr else false", is that it is followed by something that's evaluated as boolean; and all exception types and tuples will be true in that context.
Notably, the "say it like you would in English" that convinced Perl still applies: "if" *without* a "then" is normally an extra condition added after the main point:
Normally ham, but fish if it's a Friday.
That's not how Python words ternary if, though. "Ham, if it's not Friday; otherwise fish" is closer, or inverting that: "Fish on Fridays, but normally ham". English is pretty flexible with how you lay things out :)
value = expr except (Exception [as e]: default)
(and the similar but unmentioned)
value = expr except (Exception [as e] -> default)
The parenthesizing question and the choice of tokens are considered independent, so not all the cross-multiplications are listed.
The mapping analogy for ":" is good -- and is the reason to place parentheses there, as opposed to around the whole expression. Your preferred form -- without the internal parentheses -- looks very much like a suite-introduction, and not at all like the uses where an inline colon is acceptable.
I have some notes on that down the bottom: http://www.python.org/dev/peps/pep-0463/#colons-always-introduce-suites
value = expr except Exception [as e] continue with default
This one works for me, but only because I read "continue with" as a compound keyword. I assume the parser would too. :D But I do recognize that it is a poor choice for those who see the space as a more solid barrier.
That depends on all parsers (computer and human) being okay with the two-keyword unit. Would have to poll human parsers to see how they feel about it.
value = expr except(Exception) default # Catches only the named type(s)
This looks too much like the pre-"as" way of capturing an exception.
Not sure what the connection is, but I don't like the style anyway: putting an expression immediately after a close parenthesis seems odd (I can't think of anything else in Python that has that).
value = default if expr raise Exception
(Without new keyword "raises",) I would have to work really hard not to read that as:
__temp = default if expr: raise Exception value = __temp
Yeah; on the flip side, "raises" doesn't add a huge amount of clarity, and it's creating a new keyword that's not identical, but so all-but - oh Saphir, is it not quite too "all-but"?
value = expr or else default if Exception
To me, this just seems like a wordier and more awkward version of
expr except (default if Exception [as e])
including the implicit parentheses around "default if Exception".
And mainly, it abuses three keywords that can all already exist in an expression, and doesn't use either "try" or "except". Suppose you saw that, and wanted to know what it does. What would you search the docs for?
value = expr except Exception [as e] -> default
Without parens to group Exception and default, this looks too much like an annotation describing what the expr should return.
Can expressions have annotations?
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.
Nick is right that you should specify whether it is deferred or rejected, because the simplest implementation may lock you into too broad a scope if it is added later.
Put it this way: It's deferred until there's a non-closure means of creating a sub-scope. If that never happens, then this is postponed indefinitely (like pay-day for the Dormouse's good workers); and that doesn't majorly bother me, because the "as" clause isn't very much used in the simple try/except that want to be made into expressions. Philosophically it's a nice thing to support, so it's not Rejected; but it's dependent on a feature that CPython doesn't have, and may never have. In theory, someone could implement subscopes in a different Python, and then reopen this part of the proposal as "CPython won't support it, but MyPy will, can we make this official please?"; I don't know that anyone would bother, but it'd be plausible with the syntax as given.
The four forms most supported by this proposal are, in order::
value = (expr except Exception: default) value = (expr except Exception -> default)
If there are not parentheses after "except", it will be very tempting (and arguably correct) to (at least mentally) insert them around the first two clauses -- which are evaluated first. But that leaks into
value = (expr except Exception): default
which strongly resembles the suite-starter ":", but has very little in common with the mapping ":" or the signature ":".
value = (expr except Exception) -> default
which looks like an annotation, rather than a part of the value-determination.
value = true_expr if cond else false_expr You can put parens around true_expr or cond, no problem, but you can't put them around both:
value = (true_expr if cond) else false_expr SyntaxError: invalid syntax
Why would you want to put them around the first two expressions here? It's exactly the same: you have a ternary operator, so you can't parenthesize two of its terms and one of its keywords. That breaks up the parse unit, visually and logically. So that simply wouldn't work. :) ChrisA
(Thu Mar 6 23:26:47 CET 2014) Chris Angelico responded:
On Fri, Mar 7, 2014 at 7:29 AM, Jim J. Jewett <jimjjewett at gmail.com> wrote:
[ note that "x if y" already occurs in multiple contexts, and always evaluates y before x. ]
Yes, but that's still out of order.
Yeah, but local consistency is more important than global guidelines. :D
... *re*-using out-of-order-"if" shouldn't add any additional costs.
The other thing to note is that it's somewhat ambiguous. Until you find that there isn't an else clause, it could be the equally valid "expr except (default if cond else other_default)", with the actual "if Exception" part still to come.
True -- and I'm not a parser expert. But my belief is that the current parser allows lookahead for exactly one token, and that the "else" would fit within that limit.
... humans reading the code have to assume style guides mightn't be followed.
True ... but I hope any non-trivial use of this (including use with a non-trivial ternary if) will look bad enough to serve as its own warning.
The advantages of this form get much stronger with [as e] or multiple different except clauses, but some of them do apply to even the simplest form.
Multiple different except clauses would make for an even messier evaluation order:
expr1 except expr3 if expr2 except expr5 if expr4
If you consider the exception type to be the condition, then this makes sense (that is, if you read it as "if isinstance(thrown_exception, Exception)"); [but the most obvious reading is boolean; as always True]
I phrased that badly. I agree that without parentheses for good spacing, the above is at least ambiguous -- that is what you get for stringing multiple clauses together without internal grouping. I do think parentheses help, (but are less important when there is only a single "if") and I strongly prefer that they be internal (which you fear looks too much like calling a function named except). In that case, it is: expr1 except (expr3 if expr2) and the extension to multiple except clauses would be: expr1 except (expr3 if expr2, expr5 if expr4) though as I discuss later, placing parentheses there also makes a colon or arrow more tolerable. It does this because the nearby parens make it look more like the existing (non-lambda) uses of inline-colon to associate the two things on either side. (Without nearby brackets, the scope of the colon or arrow is more easily taken to be the whole line.) expr1 except (expr2: expr3, expr4: expr5) expr1 except (expr2 -> expr3, expr4 -> expr5)
Notably, the "say it like you would in English" that convinced Perl still applies: "if" *without* a "then" is normally an extra condition added after the main point:
Normally ham, but fish if it's a Friday.
That's not how Python words ternary if, though.
Agreed ... the "say it like you would in English" applies only to the "expr if expr" form (proposed here and) used by comprehensions: [1/x for x in data if x]
value = expr except (Exception [as e]: default)
(and the similar but unmentioned)
value = expr except (Exception [as e] -> default)
The parenthesizing question and the choice of tokens are considered independent, so not all the cross-multiplications are listed.
The mapping analogy for ":" is good -- and is the reason to place parentheses there, as opposed to around the whole expression. Your preferred form -- without the internal parentheses -- looks very much like a suite-introduction, and not at all like the uses where an inline colon is acceptable.
I have some notes on that down the bottom:
http://www.python.org/dev/peps/pep-0463/#colons-always-introduce-suites
I know that they don't always introduce suites. I can't speak to the lambda precedent, but I do know that I personally often stumble when trying to parse it, so I don't consider it a good model. The other three inline uses (dict display, slide notation, and function parameter annotation) are effectively conjunction operators, saying that expr1 and expr2 are bound more tightly than you would assume if they were separated by commas. They only occur inside a fairly close bracket (of some sort), and if the bracket isn't *very* close, then there are usually multiple associates-colons inside the same bracket. data[3:5] data[-1:-3:-1] def myfunc(a:int=5, b:str="Jim", c:float=3.14) {'b': 2, 'c': 3, 'a': 1} With parentheses after the except, the except expression will match this pattern too -- particularly if there are multiple types of exception treated differently. expr1 except (expr2: expr3) Without (preferably internal) parentheses, it will instead look like a long line with a colon near the end, and a short continuation suite that got moved up a line because it was only one statement long. def nullfunc(self, a): pass expr1 except expr3: expr2
value = expr except Exception [as e] -> default
Without parens to group Exception and default, this looks too much like an annotation describing what the expr should return.
Can expressions have annotations?
Not yet. I have seen proposals. Other syntax (notably, Decorations) have expanded their domain. I would be mildly surprised if there isn't already a 3rd party template system using -> in places where it isn't currently defined.
All forms involving the 'as' capturing clause have been deferred ...
Nick is right that you should specify whether it is deferred or rejected, because the simplest implementation may lock you into too broad a scope if it is added later.
Put it this way: It's deferred until there's a non-closure means of creating a sub-scope.
The problem is that once it is deployed as leaking into the parent scope, backwards compatibility may force it to always leak into the parent scope. (You could document the leakage as a bug or as implementation-defined, but ... those choices are also sub-optimal.)
The four forms most supported by this proposal are, in order::
value = (expr except Exception: default) value = (expr except Exception -> default)
If there are not parentheses after "except", it will be very tempting (and arguably correct) to (at least mentally) insert them around the first two clauses -- which are evaluated first. But that leaks into
value = (expr except Exception): default
...
You can put parens around true_expr or cond, no problem, but you can't put them around both:
value = (true_expr if cond) else false_expr SyntaxError: invalid syntax
The fact that short-circuiting will prevent false_expr from even being evaluated means that "(true_expr if cond)" is a useful mental approximation, even though "true_expr if cond" is already to the left. Commutivity and Associativity are pretty strong biases; testing order of execution for apparently parallel expressions (such as different arguments to the same function in the same call) is a source of trick questions. When associativity is violated, computation normally proceeds from the left, and any time you need to jump ahead to simplify something that isn't left-most, that special case needs to be signalled. For short enough jumps, order of operations can be sufficient (e=mc^2), but you'll still see plenty of programmers inserting redundant parentheses just to make things clear (or maybe just to avoid having to look up the order of operations). The replacement value and the exception that triggers it are clearly associated more closely with each other than either is to the primary expression. Therefore, they *should* be grouped more tightly. This is a proposal with no perfect syntax, but I don't really see any upside to *not* grouping the exception with its exceptional result. At the moment, I don't know how to spell that closer grouping without at least one of: Parentheses or brackets of some sort A line break/new suite: - kills the proposal An inline associates-colon: - needs brackets anyhow, or it will be mistaken for a suite-introduction-colon. A new grouping operator, such as "->" - risk that it will be misinterpreted Relying on convention and taste: (main_expr except exc_expr fallback_expr) -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 Sat, Mar 8, 2014 at 5:58 AM, Jim J. Jewett <jimjjewett@gmail.com> wrote:
(Thu Mar 6 23:26:47 CET 2014) Chris Angelico responded:
On Fri, Mar 7, 2014 at 7:29 AM, Jim J. Jewett <jimjjewett at gmail.com> wrote:
[ note that "x if y" already occurs in multiple contexts, and always evaluates y before x. ]
Yes, but that's still out of order.
Yeah, but local consistency is more important than global guidelines. :D
I don't see except expressions as fundamentally more associated with if/else than with, say, an or chain, which works left to right. Aside from both being ternary operators, of course, which isn't really much of a justification.
The other thing to note is that it's somewhat ambiguous. Until you find that there isn't an else clause, it could be the equally valid "expr except (default if cond else other_default)", with the actual "if Exception" part still to come.
True -- and I'm not a parser expert. But my belief is that the current parser allows lookahead for exactly one token, and that the "else" would fit within that limit.
... humans reading the code have to assume style guides mightn't be followed.
True ... but I hope any non-trivial use of this (including use with a non-trivial ternary if) will look bad enough to serve as its own warning.
Not sure whether the parser would be able to handle it or not, but the human would have to if the machine can, and that's going to be a source of confusion. I'd rather avoid it if I can. Remember, everywhere else in Python, the word "if" is followed by something that's interpreted as a boolean. You wouldn't expect to see this somewhere: if None: do_stuff() do_stuff() if sys else do_other_stuff() So it'd cause a mental glitch to see some other constant and always-true expression there: ... if ZeroDivisionError ... You don't expect ZeroDivisionError ever to be false. Seeing it following an if would leave you wondering if maybe it is. IMO that syntax is abuse of the 'if' keyword, hammering it into a hole it's not designed to fit into.
I do think parentheses help, (but are less important when there is only a single "if")
Analysis of the Python standard library suggests that the single-if situation is *by far* the most common, to the extent that it'd hardly impact the stdlib at all to add multiple except clauses to the proposal. Do you have a strong use-case for the more full syntax? It tends to get long, which rather damages the readability. A number of people have said that if the except expression goes over a line break, it should probably be a statement instead. I'm not sure that extreme is fully justified, but it certainly does have merit. In a codebase of mine I wouldn't *forbid* breaking an except expression over multiple lines, but it'd certainly be a reason to consider whether it's really necessary or not.
and I strongly prefer that they [the parentheses] be internal (which you fear looks too much like calling a function named except). In that case, it is:
expr1 except (expr3 if expr2)
I'm still not really seeing how this is better. With the colon version, it looks very much like dict display, only with different brackets around it; in some fonts, that'll be very easily confused. With the if, it looks like an incomplete expression waiting for its else. And I'm still not enamored of constructing syntax that has the evaluation order (a) not simple left-to-right, like most expressions are, and (b) different from the except statement, which puts the exception_list ahead of the suite.
Agreed ... the "say it like you would in English" applies only to the "expr if expr" form (proposed here and) used by comprehensions:
[1/x for x in data if x]
Sure. That usage makes a _lot_ of sense, I really like list comprehensions. There's no room in them for an 'else' clause though, so it's very strictly one-way. Makes them a tricky comparison for this.
I can't speak to the lambda precedent, but I do know that I personally often stumble when trying to parse it, so I don't consider it a good model.
The only confusion I have with lambda is its precedence, which trips me up now and then:
def f(x): ... return lambda y: x,y ... f(5) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in f NameError: name 'y' is not defined
That's not returning a function that returns a tuple. It's returning a tuple of a function and the global y, and the function will ignore its arg.
The other three inline uses (dict display, slide notation, and function parameter annotation) are effectively conjunction operators, saying that expr1 and expr2 are bound more tightly than you would assume if they were separated by commas. They only occur inside a fairly close bracket (of some sort), and if the bracket isn't *very* close, then there are usually multiple associates-colons inside the same bracket.
Not sure that really helps. This isn't going to be as tight as a slice, and it's most often not going to have multiple colons inside the brackets.
Without (preferably internal) parentheses, it will instead look like a long line with a colon near the end, and a short continuation suite that got moved up a line because it was only one statement long.
def nullfunc(self, a): pass
expr1 except expr3: expr2
That should have expr2 and exp3 the other way round - the bit before the colon is evaluated before the bit after the colon. What about putting it all in parens, which is the current proposal? There's no way you can have a statement with a one-line suite on the same line, all enclosed in parens. (expr1 except expr2: expr3)
All forms involving the 'as' capturing clause have been deferred ...
Nick is right that you should specify whether it is deferred or rejected, because the simplest implementation may lock you into too broad a scope if it is added later.
Put it this way: It's deferred until there's a non-closure means of creating a sub-scope.
The problem is that once it is deployed as leaking into the parent scope, backwards compatibility may force it to always leak into the parent scope. (You could document the leakage as a bug or as implementation-defined, but ... those choices are also sub-optimal.)
It'll never be deployed as leaking, for the same reason that the current 'except' statement doesn't leak: it creates a refloop. The entire notion of an 'as' clause is deferred. If there's no way to create a sub-scope that isn't a closure, then there will be no 'as' clause in an except expression. It's that simple. Then later, if such a feature is added, it can be retrofitted without breaking anything. But there's little need of it; the cases where the exception needs to be captured are usually the ones that need the full statement anyway. Of all 'except' statements in the stdlib, almost exactly one in five uses 'as'; of the ones simple enough to potentially be changed to this form, just 7 out of 236 do. (And those seven are among the more dubious ones.) So it's no great loss to the proposal, assuming the stdlib is indicative. (If you have a good-sized Py3 codebase to try it on, feel free to enhance those stats with more data. The script's part of my repo.)
The fact that short-circuiting will prevent false_expr from even being evaluated means that "(true_expr if cond)" is a useful mental approximation, even though "true_expr if cond" is already to the left.
Hrm, I don't really like that way of describing it. To me, the entire expression is one unit - you can't parenthesize it. It's like parenthesizing a chained comparison: if 5 < x < 10: if (5 < x) < 10: if 5 < (x < 10): You can't add parens around one part of the expression; it completely changes the meaning of it.
Commutivity and Associativity are pretty strong biases; testing order of execution for apparently parallel expressions (such as different arguments to the same function in the same call) is a source of trick questions.
In C, the answer is usually "Implementation-defined". In Python, the answer is usually "Left to right".
The replacement value and the exception that triggers it are clearly associated more closely with each other than either is to the primary expression. Therefore, they *should* be grouped more tightly.
Maybe. On the other hand, the replacement value and the original expression are two things of the same broad type, and the other expression is an exception list, so the primary and the replacement should be grouped more tightly. You're going to get this or that. I don't think this is a strong reason for parentheses.
This is a proposal with no perfect syntax, but I don't really see any upside to *not* grouping the exception with its exceptional result. At the moment, I don't know how to spell that closer grouping without at least one of:
Parentheses or brackets of some sort
A line break/new suite: - kills the proposal
An inline associates-colon: - needs brackets anyhow, or it will be mistaken for a suite-introduction-colon.
A new grouping operator, such as "->" - risk that it will be misinterpreted
Relying on convention and taste: (main_expr except exc_expr fallback_expr)
Definitely not the last one. It needs some kind of syntactic separation, not just whitespace (what if the fallback_expr is a negative number?). The entire expression should be representing a single thought, or else it probably should be written as a statement. Like the thought "Get me the current working directory, None if there isn't one". pwd = (os.getcwd() except OSError: None) Or: "Append the IP, or failing that the network address". ips.append(ip.ip except AttributeError: ip.network_address) With those, there's really no separation between "Get the current working directory", "if there isn't one", and "None". Parenthesizing the part after 'except' makes good sense if you want to have multiple except clauses, but there's no reason to do that, so we're back with this very simple case. I'd rather not mandate any parens at all, personally, but as was said in another thread, you don't need to see a Frenchman on the parapet to know where the wind's blowing. ChrisA
TL;DR: expr except (default if exc_expr) expr (except default if exc_expr) expr except (exc_expr: default) expr (except exc_expr: default) (1) Group the exceptions with the default they imply. (2) inline-":" still needs () or [] or {}. (3) Consider the expression inside a longer line. (3a) Does the except expression need to be general, or would it work if it were limited to a subclause of variable assignments? (3b) What about comprehensions? On Fri Mar 7 20:54:31 CET 2014, Chris Angelico wrote:
On Sat, Mar 8, 2014 at 5:58 AM, Jim J. Jewett <jimjjewett at gmail.com> wrote:
(Thu Mar 6 23:26:47 CET 2014) Chris Angelico responded:
On Fri, Mar 7, 2014 at 7:29 AM, Jim J. Jewett <jimjjewett at gmail.com> wrote:
[ note that "x if y" already occurs in multiple contexts, and always evaluates y before x. ]
...
I don't see except expressions as fundamentally more associated with if/else than with, say, an or chain, which works left to right.
I do, because of the skipping portion. Short-circuiting operators, such as an "or" chain, never skip a clause unless they are skipping *every* subsequent clause. An "if" statement sometimes skips the (unlabeled in python) "then" clause, but still processes the even-later "else" clause. A "try" statement sometimes skips the remainder of the try suite but still executes the later subordinate "except" and "finally" clauses. Note that this only explains why I see "except" as more closely related to "if" than to "or"; it isn't sufficient to justify going back to execute the skipped clause later. That said, going back to a previous location is a lot easier to excuse after an error handler than in "regular" code.
Analysis of the Python standard library suggests that the single-if situation is *by far* the most common, to the extent that it'd hardly impact the stdlib at all to add multiple except clauses to the proposal. Do you have a strong use-case for the more full syntax?
I do not. I dislike the arbitrary restriction, and I worry that lifting it later (while maintaining backwards compatibility) will result in a syntax wart, but I do not have a compelling use case for that later relaxation.
and I strongly prefer that they [the parentheses] be internal (which you fear looks too much like calling a function named except). In that case, it is:
expr1 except (expr3 if expr2)
I'm still not really seeing how this is better.
For one thing, it makes it clear that the "if" keyword may be messing with the order of evaluation. I don't claim that syntax is perfect. I do think it is less flawed than the no-parentheses (or external parentheses) versions: (expr1 except expr3 if expr2) expr1 except expr3 if expr2 because the tigher parentheses correctly indicate that expr2 and expr3 should be considered as a (what-to-do-in-case-of-error) group, which interacts (as a single unit) with the main expression. I also think it is (very slighly) better than the colon+internal-parentheses version: expr1 except (expr2: expr3) which in turn is far, far better than the colon versions with external or missing parentheses: (expr1 except expr2: expr3) expr1 except expr2: expr3 because I cannot imagine reading an embedded version of either of those without having to mentally re-parse at the colon. An example assuming a precedence level that may not be what the PEP proposes: if myfunc(5, expr1 except expr2: expr3, "label"): for i in range(3, 3*max(data) except TypeError: 9, 3): ... if myfunc(5, (expr1 except expr2: expr3), "label"): for i in range(3, (3*max(data) except TypeError: 9), 3): ... if myfunc(5, expr1 except (expr2: expr3), "label"): for i in range(3, 3*max(data) except (TypeError: 9), 3): ... if myfunc(5, expr1 except (expr2: expr3), "label"): for i in range(3, 3*max(data) (except TypeError: 9), 3): ... if myfunc(5, expr1 except (expr3 if expr3), "label"): for i in range(3, 3*max(data) (except 9 if TypeError), 3): ... if myfunc(5, expr1 except (expr3 if expr3), "label"): for i in range(3, 3*max(data) except (9 if TypeError), 3): myarg = expr1 except (expr3 if expr2) if myfunc(5, myarg, "label"): limit = 3*max(data) except (9 if TypeError) for i in range(3, limit, 3): Yes, I would prefer to create a variable naming those expressions, but these are all still simple enough that I would expect to have to read them. (I like constructions that get ugly just a bit faster than they get hard to understand.) If I have to parse any of them, the ones at the bottom are less difficult than the ones at the top.
With the colon version, it looks very much like dict display,
which is good, since that is one of the acceptable uses of inline-colon.
only with different brackets around it; in some fonts, that'll be very easily confused.
I've had more trouble with comma vs period than with different types of bracket. But let's assume that there is confusion, and someone sees: expr1 except [expr2:expr3] or expr1 except {expr2:expr3} These are not yet defined any more than the tuple-with-colon version is, nor do they have an obvious-yet-incompatible meaning. In fact, I would prefer either of them to any version that does not syntactically associate the exception list with the result those exceptions imply.
The other three inline uses (dict display, slide notation, and function parameter annotation) are effectively conjunction operators, saying that expr1 and expr2 are bound more tightly than you would assume if they were separated by commas. They only occur inside a fairly close bracket (of some sort), and if the bracket isn't *very* close, then there are usually multiple associates-colons inside the same bracket.
Not sure that really helps. This isn't going to be as tight as a slice, and it's most often not going to have multiple colons inside the brackets.
The colon is acceptable only to the extent that this similarity does help. So if a colon is used, I want the similarity to be as strong as possible -- which, I suppose, is another argument for tightening the parentheses, and possibly an argument for using [] instead of (). -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 Mon, Mar 10, 2014 at 1:16 PM, Jim J. Jewett <jimjjewett@gmail.com> wrote:
On Fri Mar 7 20:54:31 CET 2014, Chris Angelico wrote:
I don't see except expressions as fundamentally more associated with if/else than with, say, an or chain, which works left to right.
I do, because of the skipping portion.
Short-circuiting operators, such as an "or" chain, never skip a clause unless they are skipping *every* subsequent clause.
An "if" statement sometimes skips the (unlabeled in python) "then" clause, but still processes the even-later "else" clause.
A "try" statement sometimes skips the remainder of the try suite but still executes the later subordinate "except" and "finally" clauses.
Note that this only explains why I see "except" as more closely related to "if" than to "or"; it isn't sufficient to justify going back to execute the skipped clause later. That said, going back to a previous location is a lot easier to excuse after an error handler than in "regular" code.
This is a rather tenuous connection, I think. It also doesn't justify a fundamentally out-of-order evaluation. Compare all of these, which are - unless I've made a mistake somewhere - evaluated in the order shown (that is, no lower-numbered will be evaluated after a higher-numbered, but of course not everything will be evaluated): if expr1: stmt2 elif expr3: stmt4 else: stmt5 try: stmt1 except expr2: stmt3 except expr4: stmt5 except: stmt6 else: stmt7 finally: stmt8 value = expr1 or expr2 or expr3 or expr4 value = expr1 and expr2 and expr3 and expr4 # With the current recommendations in PEP 463 value = (expr1 except expr2: expr3) value = expr2 if expr1 else expr3 The if expression is the *only* one that's out of order, and it's justified by (a) "reading" correctly (ie being similar to English), and (b) using two existing keywords, rather than creating a 'then' keyword or using symbols. Useful reading: http://legacy.python.org/dev/peps/pep-0308/ Skip down to the "Detailed Results of Voting" section (sorry, text/plain PEPs don't do hash links) and note that these four proposals received the most support: A. x if C else y B. if C then x else y C. (if C: x else: y) D. C ? x : y with more support for C and D than for A and B. B requires a new keyword, D is all symbols, C is indistinguishable from the statement without the parens, and A puts the expressions in the wrong order. All of them are somewhat flawed but all of them have some merit, too. The BDFL went for A (which proves that Python isn't a democracy), and there's no explanation of the reasoning behind that, but in the absence of a strong statement on the subject, I would say that the gap between the above four options is sufficiently narrow that this can't be used to justify out-of-order syntax in any other proposal.
Analysis of the Python standard library suggests that the single-if situation is *by far* the most common, to the extent that it'd hardly impact the stdlib at all to add multiple except clauses to the proposal. Do you have a strong use-case for the more full syntax?
I do not.
I dislike the arbitrary restriction, and I worry that lifting it later (while maintaining backwards compatibility) will result in a syntax wart, but I do not have a compelling use case for that later relaxation.
That's one reason for the mandatory external parentheses. With some real-world usage data from Python 3.5, if this were implemented exactly per the PEP, a decision could be made in 3.6 to lift one of two restrictions: 3.5: (expr1 except Exception2: expr3) 3.6 choice 1: expr1 except Exception2: expr3 3.6 choice 2: (expr1 except Exception2: expr3 except Exception4: expr5) But these are incompatible. Lifting either restriction mandates the retention of the other. Note also that internal parens are less easily removed - it becomes two distinct code branches. Removing the requirement to have external parents simply means that old-style code is putting parens around an expression, no different from: value = (1 if x else 2) value = (3 + 5) and completely harmless. Allowing optional internal parens means maintaining two syntaxes forever, and both human and computer parsers would have to be compatible with both.
and I strongly prefer that they [the parentheses] be internal (which you fear looks too much like calling a function named except). In that case, it is:
expr1 except (expr3 if expr2)
I'm still not really seeing how this is better.
For one thing, it makes it clear that the "if" keyword may be messing with the order of evaluation.
It still could be waiting for its else. I mean, it's bad code style, but you theoretically could do this: foo[x] except (IndexError if isinstance(foo,list) else KeyError): None Really bad style and horribly unclear (and anyway, you could just catch LookupError), but grammatically legal. Anything using the word 'if' in an expression context has to consider that. (Note that the converse of using 'else' on its own wouldn't be a problem, as it wouldn't be following an if.)
I don't claim that syntax is perfect. I do think it is less flawed than the no-parentheses (or external parentheses) versions:
(expr1 except expr3 if expr2) expr1 except expr3 if expr2
because the tigher parentheses correctly indicate that expr2 and expr3 should be considered as a (what-to-do-in-case-of-error) group, which interacts (as a single unit) with the main expression.
But it doesn't, really. The entire set of three expressions is a single unit. You can't break out the bit inside the parens and give that a name, like you can in most places where something "acts as a single unit" to interact with something else. (Yes, there are special cases, like the syntax for constructing slice objects that works only inside square brackets. And you can't break out a function's arguments, as a unit, into a single object (the nearest is *args,**kw). I said most places, and I don't want to add more to the special-case set.)
which in turn is far, far better than the colon versions with external or missing parentheses:
(expr1 except expr2: expr3) expr1 except expr2: expr3
because I cannot imagine reading an embedded version of either of those without having to mentally re-parse at the colon.
What do you mean by "re-parse"? The general recommendation is that the entire except expression should fit inside a human's 7 +/- 2 limit. Fundamentally, a good use of this syntax will still be saying basically the same thing at the end that it seemed to be saying at the beginning, in the same way that short-circuiting or can be used.
dic = {7:"Seven", 9:None, 5:"Five"} dic[7] or "Default" 'Seven' dic[9] or "Default" 'Default'
It's still looking something up, but now it adds a default for the case where the stored value is falsy. Now compare the (slightly more verbose) version that catches an exception:
(dic[3] except KeyError: "Not present") 'Not present'
Again, it's still looking something up, but now coping with an additional situation.
I've had more trouble with comma vs period than with different types of bracket. But let's assume that there is confusion, and someone sees:
expr1 except {expr2:expr3}
(most fonts will make square brackets clear enough, it's only braces and round brackets that are problematic)
These are not yet defined any more than the tuple-with-colon version is, nor do they have an obvious-yet-incompatible meaning. In fact, I would prefer either of them to any version that does not syntactically associate the exception list with the result those exceptions imply.
Actually, braces with colons inside *is* defined. What's not defined is the word "except" followed by a dictionary. But at the point where you're reading that, it looks like a dict. The trouble is that that's *almost* valid; you can think of the exception handling as mapping an exception to its result. But that's not valid for two reasons: firstly, exception handling is inherently ordered, and secondly, the expressions are not evaluated eagerly, as they would be in a dict. Only the one that matches will be evaluated. Making the syntax look like it's laying out a dictionary would be awkwardly confusing.
The colon is acceptable only to the extent that this similarity does help. So if a colon is used, I want the similarity to be as strong as possible -- which, I suppose, is another argument for tightening the parentheses, and possibly an argument for using [] instead of ().
Tightening them doesn't particularly help, but is an arguable point. But using square brackets? Definitely not. Parentheses are used syntactically in many places (grouping, function calls, and they're associated with tuples), but square brackets are not; plus, see argument above about the option of relaxing the restrictions. Using square brackets would be confusing, and would often be interpreted as creating a list. Not something we want to imply. ChrisA
On Mon, 10 Mar 2014 14:26:14 +1100, Chris Angelico <rosuav@gmail.com> wrote:
On Mon, Mar 10, 2014 at 1:16 PM, Jim J. Jewett <jimjjewett@gmail.com> wrote:
I don't claim that syntax is perfect. I do think it is less flawed than the no-parentheses (or external parentheses) versions:
(expr1 except expr3 if expr2) expr1 except expr3 if expr2
because the tigher parentheses correctly indicate that expr2 and expr3 should be considered as a (what-to-do-in-case-of-error) group, which interacts (as a single unit) with the main expression.
But it doesn't, really. The entire set of three expressions is a single unit. You can't break out the bit inside the parens and give that a name, like you can in most places where something "acts as a single unit" to interact with something else. (Yes, there are special cases, like the syntax for constructing slice objects that works only inside square brackets. And you can't break out a function's arguments, as a unit, into a single object (the nearest is *args,**kw). I said most places, and I don't want to add more to the special-case set.)
Actually, function arguments almost aren't a special case any more:
import inspect def a(a, b=2): ... print(a, b) ... def b(c, d=3): ... print(c, d) ... sa = inspect.signature(a) print(sa) (a, b=2) ba = sa.bind(1, 2) b(*ba.args, **ba.kwargs) 1 2
Note: I said *almost* :) But the point is that we found that the fact that we couldn't give this thing in parens a name bothersome enough to partially fix it. --David
On 03/09/2014 07:16 PM, Jim J. Jewett wrote:
because I cannot imagine reading an embedded version of either of those without having to mentally re-parse at the colon. An example assuming a precedence level that may not be what the PEP proposes:
if myfunc(5, expr1 except expr2: expr3, "label"): for i in range(3, 3*max(data) except TypeError: 9, 3): ...
if myfunc(5, (expr1 except expr2: expr3), "label"): for i in range(3, (3*max(data) except TypeError: 9), 3): ...
if myfunc(5, expr1 except (expr2: expr3), "label"): for i in range(3, 3*max(data) except (TypeError: 9), 3): ...
if myfunc(5, expr1 except (expr2: expr3), "label"): for i in range(3, 3*max(data) (except TypeError: 9), 3): ...
if myfunc(5, expr1 except (expr3 if expr3), "label"): for i in range(3, 3*max(data) (except 9 if TypeError), 3): ...
if myfunc(5, expr1 except (expr3 if expr3), "label"): for i in range(3, 3*max(data) except (9 if TypeError), 3):
myarg = expr1 except (expr3 if expr2) if myfunc(5, myarg, "label"): limit = 3*max(data) except (9 if TypeError) for i in range(3, limit, 3):
Yes, I would prefer to create a variable naming those expressions, but these are all still simple enough that I would expect to have to read them. (I like constructions that get ugly just a bit faster than they get hard to understand.) If I have to parse any of them, the ones at the bottom are less difficult than the ones at the top.
I totally disagree. I found the second one the easiest to read, and outside a function call (or other complexity, the parens wouldn't be needed. -- ~Ethan~
On Fri Mar 7 20:54:31 CET 2014, Chris Angelico wrote:
On Sat, Mar 8, 2014 at 5:58 AM, Jim J. Jewett <jimjjewett at gmail.com> wrote:
(Thu Mar 6 23:26:47 CET 2014) Chris Angelico responded:
...[as-capturing is] deferred until there's a non-closure means of creating a sub-scope.
The problem is that once it is deployed as leaking into the parent scope, backwards compatibility may force it to always leak into the parent scope. (You could document the leakage as a bug or as implementation-defined, but ... those choices are also sub-optimal.)
It'll never be deployed as leaking, for the same reason that the current 'except' statement doesn't leak:
I don't think that is the full extent of the problem. From Nick's description, this is a nasty enough corner case that there may be glitches no one notices in time. The PEP should therefore explicitly state that implementation details may force the deferral to be permanent, and that this is considered an acceptable trade-off. -jJ -- Sorry for the botched subject line on the last previous message. If there are still threading problems with my replies, please email me with details, so that I can try to resolve them. -jJ
On Mon, Mar 10, 2014 at 1:35 PM, Jim J. Jewett <jimjjewett@gmail.com> wrote:
The PEP should therefore explicitly state that implementation details may force the deferral to be permanent, and that this is considered an acceptable trade-off.
How about words to this effect? """Should there be, in future, a way to create a true subscope (which could simplify comprehensions, except expressions, with blocks, and possibly more), then this proposal could be revived; until then, its loss is not a great one...""" (I parked the time machine in the PSU's garage, thanks for lending me it!) (The PSU does not exist, and therefore does not have a garage.) ChrisA
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:", (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
According to the documentation, this behaviour is wrong. Now, you might argue that the documentation is wrong. I'm sympathetic to that argument, but *as documented now*, dict.get is documented as being logically equivalent to: try: return d[key] except KeyError: return default The fact that it actually isn't is an artifact of the specific implementation used. If it were a deliberate design choice, that design is not reflected in the documentation. Whether the current behaviour is wrong, or the documentation is wrong, is irrelevant to the question of whether or not the developers back in nineteen-ninety-whatever would have choosen to add dict.get had there been syntax for catching the KeyError in an expression. Perhaps they would have argued: "Sure, you can catch the KeyError yourself, but 'get' is a fundamental operation for mappings, and I think that dict should implement a 'get' method just to be complete." Or perhaps not. Some developers prefer minimalist APIs, some developers prefer more exhaustive APIs. Regardless of what might have happened back in 199x when dict.get was first discussed, I think we can agree that an except expression will lower the pressure on Python to add *more* "get-like" methods, or add default arguments, in the future. -- Steven
On 3/5/2014 8:15 PM, Steven D'Aprano wrote:
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]
This is a buggy special method. According to the docs for hash and __hash__ and the general convention on exceptions, a __hash__ method should return an int or raise TypeError.
... 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
This breaks the implied C invariant and makes the object 'a' incoherent and buggy
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
According to the documentation, this behaviour is wrong.
One could argue that an error raised in a special method is not raised *by* a function that uses the special method. The docs constantly assume that special methods are coded correctly. '''bool([x]) Convert a value to a Boolean, using the standard truth testing procedure. If x is false or omitted, this returns False; otherwise it returns True.''' ... unless x.__bool__ raises or returns something other than True/False -- in which case bool itself raises. TypeError: __bool__ should return bool, returned int
Now, you might argue that the documentation is wrong. I'm sympathetic to that argument, but *as documented now*, dict.get is documented as being logically equivalent to:
try: return d[key] except KeyError: return default
It appears to be actually equivalent to key_hash = hash(key) try: return d._hashlookup(key_has) except KeyError: return default
The fact that it actually isn't is an artifact of the specific implementation used. If it were a deliberate design choice,
Given that the choice is that bugs in special methods should not pass silently, I would presume that it is intentional.
that design is not reflected in the documentation.
The docs generally describe behavior in the absence of coding errors. -- Terry Jan Reedy
Steven D'Aprano wrote:
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.
I think that's supposed to mean that it won't raise KeyError as a result of the key not being in the dictionary. The actual behaviour is correct, IMO, because it avoids masking bugs, so this could probably be worded better. -- Greg
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 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 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 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
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 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~
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
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 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
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 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 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.
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 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
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 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 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 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 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 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
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 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~
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
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, 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: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: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 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.
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 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 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: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 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 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 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 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 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
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 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 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 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 =========
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 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 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 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.
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 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 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 abbreviate