[Python-ideas] Inline assignments using "given" clauses

Tim Peters tim.peters at gmail.com
Tue May 15 01:53:02 EDT 2018

>> It's been noted several times recently that the example PEP 572 gives
>> as _not_ working:
>>     total = 0
>>     progressive_sums = [total := total + value for value in data]
>> was the original use case that prompted work on the PEP.  You gotta
>> admit that's ironic ;-)

> After pondering this case further, I think it's also worth noting that that
> *particular* example could also be addressed by:
> 1. Allowing augmented assignment *expressions*
> 2. Changing the scoping rules for augmented assignment operations in general
> such that they *don't change the scope of the referenced name*
> Writing "i += n" without first declaring the scope of "i" with "i = 0",
> "nonlocal i" or "global i" is one of the most common sources of
> UnboundLocalError after all, so I'd be surprised to find anyone that
> considered the current augmented assignment scoping rules to be outside the
> realm of reconsideration.

Yes, it's worth considering.  In my experience, I don't believe I've
ever had an UnboundLocalError for which a correct fix was to add
`nonlocal`.  Usually the correct fix was to add `global`, but that's
mostly due to an old habit of using piles of globals to count trips
through various code paths, used by a watchdog thread that
periodically wakes up to display (the global) counts.

> The accumulation example would then be written:
>     total = 0
>     progressive_sums = [total += value for value in data]
>     if progressive_sums:
>         assert total == progressive_sums[-1]

And the divide-out-small-primes example could be

    factor = -42
    while any(n % (factor += p - factor) == 0 for p in small_primes):
        n //= factor

Heh ;-)

> The question would then turn to "What if you just want to bind the target
> name, without considering the old value?". And then *that's* where "NAME : =
> EXPR" would come in: as an augmented assignment operator that used augmented
> assignment scoping semantics, rather than regular local name binding
> semantics.

Plain old ":=" would somehow be viewed as being an augmented
assignment operator too? ... OK, the meaning is that augmented
assignment _and_ "::=" would resolve the target's scope in the way the
containing block resolves it.

> That would mean *directly* overturning PEP 3099's rejection of the idea of
> using "NAME := EXPR" to imply "nonlocal NAME" at function scope, but that's
> effectively on the table for implicit functions anyway (and I'd prefer to
> have ":=" be consistent everywhere, rather than having to special case the
> implicit scopes).

Creating a local in a containing scope by magic is never done by
Python today.  Extending that beyond "need" seems potentially
perilous.  For example, it can already be tedious to figure out which
names _are_ local to a function by staring at the function's code, but
people quickly get better at that over time; change the rules so that
they _also_ have to stare at all immediately contained  functions too
to figure it out, and it may become significantly harder (OK, I didn't
declare `x`, and a contained function did `x := 3.14` but `x` isn't
declared there either - I guess it's my `x` now).  Then again, if
they're doing that much function nesting they deserve whatever they
get ;-)

Restrict it to that only synthetically generated functions can pull
off this trick by magic (where there are real use cases to motivate
it), and they still don't have to look outside the body of a
function's text to figure it out.  Visually, there's no distinction
between the code running in the function's scope and in scopes
synthesized to implement comprehensions appearing in the function's
text.  The comprehensions aren't even indented more.

So, offhand, I'm not sure that the right way to address something you
view as a wart is to vastly expand its reach to 12 operators that
impose it on everyone everywhere every time they're used ;-)

Seriously, I do suspect that in

    def f(...):
        ... no instances of `s` ...
        s += f"START {time.time():.2f}"

it's overwhelmingly more likely that they simply forgot to do

        s = ""

earlier in `f` than they actually wanted to append to whatever `s`
means in f's parent block..  That's a radical change to what people
have come to expect `NAME +=` to do.

OTOH, I don't (yet?) see a way the change could break code that
currently works, so it remains worth thinking about.

BTW, would

    def f():
        x := 3.14
        x = 3.14

be a compile-time error?  Everyone agreed the analogous case would be
in synthetic functions.  Fine by me!

More information about the Python-ideas mailing list