[Python-Dev] Why not make frames? [was: Alternative forms [was: PEP 463: Exception-catching expressions]]

Chris Angelico rosuav at gmail.com
Mon Mar 10 04:26:14 CET 2014


On Mon, Mar 10, 2014 at 1:16 PM, Jim J. Jewett <jimjjewett at 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


More information about the Python-Dev mailing list