On 11 April 2018 at 15:32, Chris Angelico <ros...@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.

Thanks for putting this revised version together! You've already
incorporated my feedback on semantics, so my comments below are mostly
about the framing of the proposal in the context of the PEP itself.

> 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])

Leading with these kinds of examples really doesn't help to sell the
proposal, since they're hard to read, and don't offer much, if any,
benefit over the status quo where assignments (and hence the order of
operations) need to be spelled out as separate lines.

Instead, I'd suggestion going with the kinds of examples that folks
tend to bring up when requesting this capability:

    # Handle a matched regex
    if (match := pattern.search(data)) is not None:

    # A more explicit alternative to the 2-arg form of iter() invocation
    while (value := read_next_item()) is not None:

    # Share a subexpression between a comprehension filter clause and its output
    filtered_data = [y for x in data if (y := f(x)) is not None]

All three of those examples share the common characteristic that
there's no ambiguity about the order of operations, and the latter two
aren't amenable to simply being split out into separate assignment
statements due to the fact they're part of a loop.

A good proposal should have readers nodding to themselves and thinking
"I could see myself using that construct, and being happy about doing
so", rather than going "Eugh, my eyes, what did I just read?" :)

> The name ``prefix`` is thus searched for at global scope, ignoring the class
> name. Under the proposed semantics, this name will be eagerly bound, being
> approximately equivalent to::
>     class X:
>         names = ["Fred", "Barney", "Joe"]
>         prefix = "> "
>         def <listcomp>(prefix=prefix):
>             result = []
>             for name in names:
>                 result.append(prefix + name)
>             return result
>         prefixed_names = <listcomp>()

"names" would also be eagerly bound here.

> Recommended use-cases
> =====================
> Simplifying list comprehensions
> -------------------------------
> These list comprehensions are all approximately equivalent::
>     # Calling the function twice
>     stuff = [[f(x), x/f(x)] for x in range(5)]
>     # External helper function
>     def pair(x, value): return [value, x/value]
>     stuff = [pair(x, f(x)) for x in range(5)]
>     # Inline helper function
>     stuff = [(lambda y: [y,x/y])(f(x)) for x in range(5)]
>     # Extra 'for' loop - potentially could be optimized internally
>     stuff = [[y, x/y] for x in range(5) for y in [f(x)]]
>     # Iterating over a genexp
>     stuff = [[y, x/y] for x, y in ((x, f(x)) for x in range(5))]
>     # Expanding the comprehension into a loop
>     stuff = []
>     for x in range(5):
>         y = f(x)
>         stuff.append([y, x/y])
>     # Wrapping the loop in a generator function
>     def g():
>         for x in range(5):
>             y = f(x)
>             yield [y, x/y]
>     stuff = list(g())
>     # Using a mutable cache object (various forms possible)
>     c = {}
>     stuff = [[c.update(y=f(x)) or c['y'], x/c['y']] for x in range(5)]
>     # Using a temporary name
>     stuff = [[y := f(x), x/y] for x in range(5)]

The example using the PEP syntax could be listed first in its own
section, and then the others given as "These are the less obvious
alternatives that this new capability aims to displace".

Similar to my suggestion above, you may also want to consider making
this example a filtered comprehension in order to show the proposal in
its best light:

    results = [(x, y, x/y) for x in input_data if (y := f(x) )]

> Capturing condition values
> --------------------------
> Assignment expressions can be used to good effect in the header of
> an ``if`` or ``while`` statement::

Similar to the comprehension section, I think this part could benefit
from switching the order of presentation.

> Frequently Raised Objections
> ============================

There needs to be a subsection here regarding the need to call `del`
at class and module scope, just as there is for loop iteration
variables at those scopes.

> This could be used to create ugly code!
> ---------------------------------------
> So can anything else.  This is a tool, and it is up to the programmer to use 
> it
> where it makes sense, and not use it where superior constructs can be used.

This argument will be strengthened by making the examples used in the
PEP itself more attractive, as well as proposing suitable additions to
PEP 8, such as:

1. If either assignment statements or assignment expressions can be
used, prefer statements
2. If using assignment expressions would lead to ambiguity about
execution order, restructure to use statements instead


Nick Coghlan   |   ncogh...@gmail.com   |   Brisbane, Australia
Python-ideas mailing list
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to