[Python-ideas] PEP 572: Assignment Expressions (post #4)
Clint Hepner
clint.hepner at gmail.com
Wed Apr 11 08:23:57 EDT 2018
> On 2018 Apr 11 , at 1:32 a, Chris Angelico <rosuav at gmail.com> wrote:
>
> Wholesale changes since the previous version. Statement-local name
> bindings have been dropped (I'm still keeping the idea in the back of
> my head; this PEP wasn't the first time I'd raised the concept), and
> we're now focusing primarily on assignment expressions, but also with
> consequent changes to comprehensions.
Overall, I'm slightly negative on this. I think named expressions will
be a good thing to have, but not in this form. I'll say up front that,
being fully aware of the issues surrounding the introduction of a new
keyword, something like a let expression in Haskell would be more readable
than embedded assignments in most cases.
In the end, I suspect my `let` proposal is a nonstarter and just useful
to list with the rest of the rejected alternatives, but I wanted.
>
> Abstract
> ========
>
[...]
>
>
> Rationale
> =========
>
[...]
>
> Syntax and semantics
> ====================
>
> In any context where arbitrary Python expressions can be used, a **named
> expression** can appear. This can be parenthesized for clarity, and is of
> the form ``(target := expr)`` where ``expr`` is any valid Python expression,
> and ``target`` is any valid assignment target.
>
> The value of such a named expression is the same as the incorporated
> expression, with the additional side-effect that the target is assigned
> that value.
>
> # Similar to the boolean 'or' but checking for None specifically
> x = "default" if (eggs := spam().ham) is None else eggs
>
> # Even complex expressions can be built up piece by piece
> y = ((eggs := spam()), (cheese := eggs.method()), cheese[eggs])
I find the assignments make it difficult to pick out what the final expression looks like.
The first isn't too bad, but it took me a moment to figure out what y was. Quick: is it
* (a, b, c)
* (a, (b, c))
* ((a, b), c)
* something else
First I though it was (a, b, c), then I thought it was actually ((a, b), c), before
carefully counting the parentheses showed that I was right the first time.
These would be clearer if you could remove the assignment from the expression itself.
Assuming "let" were available as a keyword,
x = (let eggs = spam().ham
in
"default" if eggs is None else eggs)
y = (let eggs = spam(),
cheese = eggs.method()
in
(eggs, cheese, cheese[eggs]))
Allowing for differences in how best to format such an expression, the final
expression is clearly separate from its component assignment. (More on this
in the Alternative Spellings section below.)
>
> Differences from regular assignment statements
> ----------------------------------------------
>
> An assignment statement can assign to multiple targets::
>
> x = y = z = 0
>
> To do the same with assignment expressions, they must be parenthesized::
>
> assert 0 == (x := (y := (z := 0)))
There's no rationale given for why this must be parenthesized.
If := were right-associative,
assert 0 == (x := y := z := 0)
would work fine. (With high enough precedence, the remaining parentheses
could be dropped, but one would probably keep them for clarity.)
I think you need to spell out its associativity and precedence in more detail,
and explain why the rationale for the choice made.
>
> Augmented assignment is not supported in expression form::
>
>>>> x +:= 1
> File "<stdin>", line 1
> x +:= 1
> ^
> SyntaxError: invalid syntax
There's no reason give for why this is invalid. I assume it's a combination
of 1) Having both += and +:=/:+= would be redundant and 2) not wanting
to add 11+ new operators to the language.
>
> Otherwise, the semantics of assignment are unchanged by this proposal.
>
[List comprehensions deleted]
>
>
> Recommended use-cases
> =====================
>
> Simplifying list comprehensions
> -------------------------------
>
> These list comprehensions are all approximately equivalent::
[existing alternatives redacted]
> # Using a temporary name
> stuff = [[y := f(x), x/y] for x in range(5)]
Again, this would be clearer if the assignment were separated from the expression where it
would be used.
stuff = [let y = f(x) in [y, x/y] for x in range(5)]
>
> Capturing condition values
> --------------------------
>
> Assignment expressions can be used to good effect in the header of
> an ``if`` or ``while`` statement::
>
> # Current Python, not caring about function return value
> while input("> ") != "quit":
> print("You entered a command.")
>
> # Current Python, capturing return value - four-line loop header
> while True:
> command = input("> ");
> if command == "quit":
> break
> print("You entered:", command)
>
> # Proposed alternative to the above
> while (command := input("> ")) != "quit":
> print("You entered:", command)
>
> # Capturing regular expression match objects
> # See, for instance, Lib/pydoc.py, which uses a multiline spelling
> # of this effect
> if match := re.search(pat, text):
> print("Found:", match.group(0))
>
> # Reading socket data until an empty string is returned
> while data := sock.read():
> print("Received data:", data)
>
> Particularly with the ``while`` loop, this can remove the need to have an
> infinite loop, an assignment, and a condition. It also creates a smooth
> parallel between a loop which simply uses a function call as its condition,
> and one which uses that as its condition but also uses the actual value.
These are the most compelling examples so far, doing the most to push me
towards a +1. In particular, my `let` expression is too verbose here:
while let data = sock.read() in data:
print("Received data:", data)
I have an idea in the back of my head about `NAME := FOO` being syntactic
sugar for `let NAME = FOO in FOO`, but it's not well thought out.
>
>
> Rejected alternative proposals
> ==============================
>
> Proposals broadly similar to this one have come up frequently on python-ideas.
> Below are a number of alternative syntaxes, some of them specific to
> comprehensions, which have been rejected in favour of the one given above.
>
>
> Alternative spellings
> ---------------------
>
> Broadly the same semantics as the current proposal, but spelled differently.
>
> 1. ``EXPR as NAME``, with or without parentheses::
>
> stuff = [[f(x) as y, x/y] for x in range(5)]
>
> Omitting the parentheses in this form of the proposal introduces many
> syntactic ambiguities. Requiring them in all contexts leaves open the
> option to make them optional in specific situations where the syntax is
> unambiguous (cf generator expressions as sole parameters in function
> calls), but there is no plausible way to make them optional everywhere.
>
> With the parentheses, this becomes a viable option, with its own tradeoffs
> in syntactic ambiguity. Since ``EXPR as NAME`` already has meaning in
> ``except`` and ``with`` statements (with different semantics), this would
> create unnecessary confusion or require special-casing.
>
> 2. Adorning statement-local names with a leading dot::
>
> stuff = [[(f(x) as .y), x/.y] for x in range(5)] # with "as"
> stuff = [[(.y := f(x)), x/.y] for x in range(5)] # with ":="
>
> This has the advantage that leaked usage can be readily detected, removing
> some forms of syntactic ambiguity. However, this would be the only place
> in Python where a variable's scope is encoded into its name, making
> refactoring harder. This syntax is quite viable, and could be promoted to
> become the current recommendation if its advantages are found to outweigh
> its cost.
>
> 3. Adding a ``where:`` to any statement to create local name bindings::
>
> value = x**2 + 2*x where:
> x = spam(1, 4, 7, q)
>
> Execution order is inverted (the indented body is performed first, followed
> by the "header"). This requires a new keyword, unless an existing keyword
> is repurposed (most likely ``with:``). See PEP 3150 for prior discussion
> on this subject (with the proposed keyword being ``given:``).
4. Adding a ``let`` expression to create local bindings
value = let x = spam(1, 4, 7, q) in x**2 + 2*x
5. Adding a ``where`` expression to create local bindings:
value = x**2 + 2*x where x = spam(1, 4, 7, q)
Both have the extra-keyword problem. Multiple bindings are little harder
to add than they would be with the ``where:`` modifier, although
a few extra parentheses and judicious line breaks make it not so bad to
allow a comma-separated list, as shown in my first example at the top of
this reply.
>
>
> Special-casing conditional statements
> -------------------------------------
>
> One of the most popular use-cases is ``if`` and ``while`` statements. Instead
> of a more general solution, this proposal enhances the syntax of these two
> statements to add a means of capturing the compared value::
>
> if re.search(pat, text) as match:
> print("Found:", match.group(0))
>
> This works beautifully if and ONLY if the desired condition is based on the
> truthiness of the captured value. It is thus effective for specific
> use-cases (regex matches, socket reads that return `''` when done), and
> completely useless in more complicated cases (eg where the condition is
> ``f(x) < 0`` and you want to capture the value of ``f(x)``). It also has
> no benefit to list comprehensions.
>
> Advantages: No syntactic ambiguities. Disadvantages: Answers only a fraction
> of possible use-cases, even in ``if``/``while`` statements.
>
>
> Special-casing comprehensions
> -----------------------------
>
> Another common use-case is comprehensions (list/set/dict, and genexps). As
> above, proposals have been made for comprehension-specific solutions.
>
> 1. ``where``, ``let``, or ``given``::
>
> stuff = [(y, x/y) where y = f(x) for x in range(5)]
> stuff = [(y, x/y) let y = f(x) for x in range(5)]
> stuff = [(y, x/y) given y = f(x) for x in range(5)]
>
> This brings the subexpression to a location in between the 'for' loop and
> the expression. It introduces an additional language keyword, which creates
> conflicts. Of the three, ``where`` reads the most cleanly, but also has the
> greatest potential for conflict (eg SQLAlchemy and numpy have ``where``
> methods, as does ``tkinter.dnd.Icon`` in the standard library).
>
> 2. ``with NAME = EXPR``::
>
> stuff = [(y, x/y) with y = f(x) for x in range(5)]
>
> As above, but reusing the `with` keyword. Doesn't read too badly, and needs
> no additional language keyword. Is restricted to comprehensions, though,
> and cannot as easily be transformed into "longhand" for-loop syntax. Has
> the C problem that an equals sign in an expression can now create a name
> binding, rather than performing a comparison. Would raise the question of
> why "with NAME = EXPR:" cannot be used as a statement on its own.
>
> 3. ``with EXPR as NAME``::
>
> stuff = [(y, x/y) with f(x) as y for x in range(5)]
>
> As per option 2, but using ``as`` rather than an equals sign. Aligns
> syntactically with other uses of ``as`` for name binding, but a simple
> transformation to for-loop longhand would create drastically different
> semantics; the meaning of ``with`` inside a comprehension would be
> completely different from the meaning as a stand-alone statement, while
> retaining identical syntax.
>
> Regardless of the spelling chosen, this introduces a stark difference between
> comprehensions and the equivalent unrolled long-hand form of the loop. It is
> no longer possible to unwrap the loop into statement form without reworking
> any name bindings. The only keyword that can be repurposed to this task is
> ``with``, thus giving it sneakily different semantics in a comprehension than
> in a statement; alternatively, a new keyword is needed, with all the costs
> therein.
4. `` let NAME = EXPR1 in EXPR2``::
stuff = [let y = f(x) in (y, x/y) for x in range(5)]
I don't have anything new to say about this. It has the same keyword
objections as similar proposals, and I think I've addressed the use case
elsewhere.
>
> Frequently Raised Objections
> ============================
>
> Why not just turn existing assignment into an expression?
> ---------------------------------------------------------
>
> C and its derivatives define the ``=`` operator as an expression, rather than
> a statement as is Python's way. This allows assignments in more contexts,
> including contexts where comparisons are more common. The syntactic similarity
> between ``if (x == y)`` and ``if (x = y)`` belies their drastically different
> semantics. Thus this proposal uses ``:=`` to clarify the distinction.
>
>
> With assignment expressions, why bother with assignment statements?
> -------------------------------------------------------------------
>
> The two forms have different flexibilities. The ``:=`` operator can be used
> inside a larger expression; the ``=`` operator can be chained more
> conveniently, and closely parallels the inline operations ``+=`` and friends.
> The assignment statement is a clear declaration of intent: this value is to
> be assigned to this target, and that's it.
I don't find this convincing. I don't really see chained assignments often enough
to worry about how they are written, plus note my earlier question about the
precedence and associativity of :=.
The fact is, `x := 5` as an expression statement appears equivalent to the
assignment statement `x = 5`, so I suspect people will start using it as such
no matter how strongly you suggest they shouldn't.
--
Clint
More information about the Python-ideas
mailing list