[Python-ideas] PEP 572: Statement-Local Name Bindings

Rob Cliffe rob.cliffe at btinternet.com
Tue Feb 27 22:47:29 EST 2018


I hope nobody will mind too much if I throw in my (relatively 
uninformed) 2c before some of the big guns respond.

First: Well done, Chris, for all the work on this.  IMHO this could be a 
useful Python enhancement (and reduce the newsgroup churn :-)).

On 27/02/2018 22:27, Chris Angelico wrote:
> This is a suggestion that comes up periodically here or on python-dev.
> This proposal introduces a way to bind a temporary name to the value
> of an expression, which can then be used elsewhere in the current
> statement.
>
> The nicely-rendered version will be visible here shortly:
>
> https://www.python.org/dev/peps/pep-0572/
>
> ChrisA
>
> PEP: 572
> Title: Syntax for Statement-Local Name Bindings
> Author: Chris Angelico <rosuav at gmail.com>
> Status: Draft
> Type: Standards Track
> Content-Type: text/x-rst
> Created: 28-Feb-2018
> Python-Version: 3.8
> Post-History: 28-Feb-2018
>
>
> Abstract
> ========
>
> Programming is all about reusing code rather than duplicating it.  When
> an expression needs to be used twice in quick succession but never again,
> it is convenient to assign it to a temporary name with very small scope.
> By permitting name bindings to exist within a single statement only, we
> make this both convenient and safe against collisions.
It may be pedantic of me (and it *will* produce a more pedantic-sounding 
sentence) but I honestly think that
"safe against name collisions" is clearer than "safe against 
collisions", and that clarity matters.
>
>
> Rationale
> =========
>
> When an expression is used multiple times in a list comprehension, there
> are currently several suboptimal ways to spell this, and no truly good
> ways. A statement-local name allows any expression to be temporarily
> captured and then used multiple times.
IMHO the first sentence is a bit of an overstatement (though of course 
it's a big part of the PEP's "sell").
How about "... there are currently several ways to spell this, none of 
them ideal."

Also, given that a list comprehension is an expression, which in turn 
could be part of a larger expression, would it be appropriate to replace 
"expression" by "sub-expression" in the 2 places where it occurs in the 
above paragraph?
>
>
> Syntax and semantics
> ====================
>
> In any context where arbitrary Python expressions can be used, a named
> expression can appear. This must be parenthesized for clarity,
I agree, pro tem (not that I am claiming that my opinion counts for 
much).  I'm personally somewhat allergic to making parentheses mandatory 
where they really don't need to be, but trying to think about where they 
could be unambiguously omitted makes my head spin. At least, if we run 
with this for now, then making them non-mandatory in some contexts, at 
some future time, won't lead to backwards incompatibility.
>   and is of
> the form `(expr as NAME)` where `expr` is any valid Python expression,
> and `NAME` is a simple name.
>
> The value of such a named expression is the same as the incorporated
> expression, with the additional side-effect that NAME is bound to that
> value for the remainder of the current statement.
>
> Just as function-local names shadow global names for the scope of the
> function, statement-local names shadow other names for that statement.
> They can also shadow each other, though actually doing this should be
> strongly discouraged in style guides.
>
>
> Example usage
> =============
>
> These list comprehensions are all approximately equivalent::
>
>      # Calling the function twice
             # Calling the function twice (assuming that side effects 
can be ignored)
>      stuff = [[f(x), f(x)] for x in range(5)]
>
>      # Helper function
             # External helper function
>      def pair(value): return [value, value]
>      stuff = [pair(f(x)) for x in range(5)]
>
>      # Inline helper function
>      stuff = [(lambda v: [v,v])(f(x)) for x in range(5)]
>
>      # Extra 'for' loop - see also Serhiy's optimization
>      stuff = [[y, y] for x in range(5) for y in [f(x)]]
>
>      # Expanding the comprehension into a loop
>      stuff = []
>      for x in range(5):
>          y = f(x)
> stuff.append([y, y])
Please feel free to ignore this, but (trying to improve on the above 
example):
             # Using a generator:
             def gen():
                 for x in range(5):
                     y = f(x)
                     yield y,y
             stuff = list(gen())
>
>      # Using a statement-local name
>      stuff = [[(f(x) as y), y] for x in range(5)]
>
> If calling `f(x)` is expensive or has side effects, the clean operation of
> the list comprehension gets muddled. Using a short-duration name binding
> retains the simplicity; while the extra `for` loop does achieve this, it
> does so at the cost of dividing the expression visually, putting the named
> part at the end of the comprehension instead of the beginning.
Maybe add to last sentence "and of adding (at least conceptually) extra 
steps: building a 1-element list, then extracting the first element"
>
> Statement-local name bindings can be used in any context, but should be
> avoided where regular assignment can be used, just as `lambda` should be
> avoided when `def` is an option.
>
>
> Open questions
> ==============
>
> 1. What happens if the name has already been used? `(x, (1 as x), x)`
>     Currently, prior usage functions as if the named expression did not
>     exist (following the usual lookup rules); the new name binding will
>     shadow the other name from the point where it is evaluated until the
>     end of the statement.  Is this acceptable?  Should it raise a syntax
>     error or warning?
IMHO this is not only acceptable, but the (only) correct behaviour. Your 
crystal-clear statement "the new name binding will shadow the other name 
from the point where it is evaluated until the end of the statement " is 
*critical* and IMO what should happen.
Perhaps an extra example or two, to clarify that *execution order* is 
what matters, might help, e.g.
     y if (f() as y) > 0 else None
will work as expected, because "(f() as y)" is evaluated before the 
initial "y" is (if it is).

[Parenthetical comment: Advanced use of this new feature would require 
knowledge of Python's evaluation order.  But this is not an argument 
against the PEP, because the same could be said about almost any feature 
of Python, e.g.
     [ f(x), f(x) ]
where evaluating f(x) has side effects.]

2. The current implementation [1] implements statement-local names using
>     a special (and mostly-invisible) name mangling.  This works perfectly
>     inside functions (including list comprehensions), but not at top
>     level.  Is this a serious limitation?  Is it confusing?
It's great that it works perfectly in functions and list comprehensions, 
but it sounds as if, at top level, in rare circumstances it could 
produce a hard-to-track-down bug, which is not exactly desirable.  It's 
hard to say more without knowing more details.  As a stab in the dark, 
is it possible to avoid it by including the module name in the 
mangling?  Sorry if I'm talking rubbish.
>
> 3. The interaction with locals() is currently[1] slightly buggy.  Should
>     statement-local names appear in locals() while they are active (and
>     shadow any other names from the same function), or should they simply
>     not appear?
IMHO this is an implementation detail.  IMO you should have some idea 
what you're doing when you use locals().  But I think consistency 
matters - either the temporary variable *always* gets into locals() 
"from the point where it is evaluated until the end of the statement", 
or it *never* gets into locals().  (Possibly the language spec should 
specify one or the other - I'm not sure, time may tell.)
>
> 4. Syntactic confusion in `except` statements.  While technically
>     unambiguous, it is potentially confusing to humans.  In Python 3.7,
>     parenthesizing `except (Exception as e):` is illegal, and there is no
>     reason to capture the exception type (as opposed to the exception
>     instance, as is done by the regular syntax).  Should this be made
>     outright illegal, to prevent confusion?  Can it be left to linters?
>
> 5. Similar confusion in `with` statements, with the difference that there
>     is good reason to capture the result of an expression, and it is also
>     very common for `__enter__` methods to return `self`.  In many cases,
>     `with expr as name:` will do the same thing as `with (expr as name):`,
>     adding to the confusion.
This (4. and 5.) shows that we are using "as" in more than one sense, 
and in a perfect world we would use different keywords.  But IMHO 
(admittedly, without having thought about it much) this isn't much of a 
problem.  Again, perhaps some clarifying examples would help.

Some pedantry:

     One issue not so far explicitly mentioned: IMHO it should be 
perfectly legal to assign a value to a temporary variable, and then not 
use that temporary variable (just as it is legal to assign to a variable 
in a regular assignment statement, and then not use that variable) 
though linters should IMO point it out.  E.g. you might want to modify 
(perhaps only temporarily)
         a = [ (f() as b), b ]
to
         a = [ (f() as b), c ]

Also (and I'm relying on "In any context where arbitrary Python 
expressions can be used, a named expression can appear." ),
linters should also IMO point to
     a = (42 as b)
which AFAICT is a laborious synonym for
     a = 42

And here's a thought: What are the semantics of
     a = (42 as a) # Of course a linter should point this out too
At first I thought this was also a laborious synonym for "a=42". But 
then I re-read your statement (the one I described above as 
crystal-clear) and realised that its exact wording was even more 
critical than I had thought:
     "the new name binding will shadow the other name from the point 
where it is evaluated until the end of the statement"
Note: "until the end of the *statement*".  NOT "until the end of the 
*expression*".  The distinction matters.
If we take this as gospel, all this will do is create a temporary 
variable "a", assign the value 42 to it twice, then discard it. I.e. it 
effectively does nothing, slowly.
Have I understood correctly?  Very likely you have considered this and 
mean exactly what you say, but I am sure you will understand that I mean 
no offence by querying it.

Best wishes
Rob Cliffe

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20180228/81206c9e/attachment-0001.html>


More information about the Python-ideas mailing list