[Python-ideas] A comprehension scope issue in PEP 572

Guido van Rossum guido at python.org
Thu May 10 09:22:17 EDT 2018


On Thu, May 10, 2018 at 5:17 AM, Nick Coghlan <ncoghlan at gmail.com> wrote:

> On 9 May 2018 at 03:06, Guido van Rossum <guido at python.org> wrote:
>
>> So the way I envision it is that *in the absence of a nonlocal or global
>> declaration in the containing scope*, := inside a comprehension or genexpr
>> causes the compiler to assign to a local in the containing scope, which is
>> elevated to a cell (if it isn't already). If there is an explicit nonlocal
>> or global declaration in the containing scope, that is honored.
>>
>> Examples:
>>
>>   # Simplest case, neither nonlocal nor global declaration
>>   def foo():
>>       [p := q for q in range(10)]  # Creates foo-local variable p
>>       print(p)  # Prints 9
>>
>>   # There's a nonlocal declaration
>>   def bar():
>>       p = 42  # Needed to determine its scope
>>       def inner():
>>           nonlocal p
>>           [p := q for q in range(10)]  # Assigns to p in bar's scope
>>       inner()
>>       print(p)  # Prints 9
>>
>>   # There's a global declaration
>>   def baz():
>>       global p
>>       [p := q for q in range(10)]
>>   baz()
>>   print(p)  # Prints 9
>>
>> All these would work the same way if you wrote list(p := q for q in
>> range(10)) instead of the comprehension.
>>
>
> How would you expect this to work in cases where the generator expression
> isn't immediately consumed? If "p" is nonlocal (or global) by default, then
> that opens up the opportunity for it to be rebound between generator steps.
> That gets especially confusing if you have multiple generator expressions
> in the same scope iterating in parallel using the same binding target:
>
>     # This is fine
>     gen1 = (p for p in range(10))
>     gen2 = (p for p in gen1)
>     print(list(gen2))
>
>     # This is not (given the "let's reintroduce leaking from
> comprehensions" proposal)
>     p = 0
>     gen1 = (p := q for q in range(10))
>     gen2 = (p, p := q for q in gen1)
>     print(list(gen2))
>

That's just one of several "don't do that" situations. *What will happen*
is perhaps hard to see at a glance, but it's perfectly well specified. Not
all legal code does something useful though, and in this case the obvious
advice should be to use different variables.


> It also reintroduces the original problem that comprehension scopes
> solved, just in a slightly different form:
>
>     # This is fine
>     for x in range(10):
>         for y in range(10):
>             transposed_related_coords = [y, x for x, y in
> related_coords(x, y)]
>
>     # This is not (given the "let's reintroduce leaking from
> comprehensions" proposal)
>     for x in range(10):
>         for y in range(10):
>             related_interesting_coords = [x, y for x in related_x_coord(x,
> y) if is_interesting(y := f(x))]
>
> Deliberately reintroducing stateful side effects into a nominally
> functional construct seems like a recipe for significant confusion, even if
> there are some cases where it might arguably be useful to folks that don't
> want to write a named function that returns multiple values instead.
>

You should really read Tim's initial post in this thread, where he explains
his motivation. It sounds like you're not buying it, but your example is
just a case where the user is shooting themselves in the foot by reusing
variable names. When writing `:=` you should always keep the scope of the
variable in mind -- it's no different when using `:=` outside a
comprehension.

PS. Thanks for the suggestion about conflicting signals about scope; that's
what we'll do.

-- 
--Guido van Rossum (python.org/~guido)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/python-ideas/attachments/20180510/3718e466/attachment-0001.html>


More information about the Python-ideas mailing list