unintuitive for-loop behavior

Gregory Ewing greg.ewing at canterbury.ac.nz
Mon Oct 3 04:51:14 EDT 2016


Steve D'Aprano wrote:
> x = str = 1
> assert x == 1 and str == 1
> del x, str
> assert str  # succeeds
> assert x  # NameError
> x = str = 2  # create new bindings, or update existing ones?
> 
> Is it our conclusion that therefore Python creates a new binding for str
> but not for x? Or that the evidence for x is "inconclusive"? Either answer
> is illogical.

The assignment to 'str' must have created a new binding,
because the old one still exists, as evidenced by its
reappearance when we delete 'str'.

The situation as regards 'x' is inconclusive, but we can
find out by applying the closure test:

 >>> x = 1
 >>> f1 = lambda: x
 >>> del x
 >>> x = 2
 >>> f2 = lambda: x
 >>> print(f1(), f2())
(2, 2)

The second assignment to x must have updated the binding
established by the first one, because both closures see
the most recent value. (According to our definitions, this
is true *despite* the fact that we deleted x between times!)

However, things get a bit weird due to the dynamic nature
of the global and builtin namespaces:

 >>> f1 = lambda: str
 >>> str = 88
 >>> f2 = lambda: str
 >>> print(f1(), f2())
(88, 88)
 >>> del str
 >>> print(f1(), f2())
(<type 'str'>, <type 'str'>)

This is weird in a quantum-entanglement kind of way. It
appears as though the two closures have captured the
same binding, because at any given moment they both
see the same value. But *which* binding that is can
vary from one moment to the next!

I think the only way to make sense of this in terms of
the classical notions of bindings and lexical scoping
is to regard the global and builtin namespaces
together as a single lexical level, within which
dynamic lookup occurs.

By the way, Scheme implementations typically also treat
the global namespace in a more dynamic way than others.
However, in Scheme there is only a single global level,
so you don't get the same dynamic shadowing/unshadowing
behaviour.

> Locals are certainly special. The language definition singles out binding[1]
> to a variable inside a function as the trigger to treat it as a local,

As the above illustrates, there are also very real differences
between the way local and global bindings are captured
by closures. Whether those differences are considered
part of the language definition, I don't know. I suspect
Guido would regard them as artifacts of the implementation.

CPython seems to have punted on part of this by not allowing
you to delete a local name that's referenced by a nested
function, thus avoiding the issue of whether doing that should
be able to un-shadow a name further out.

-- 
Greg



More information about the Python-list mailing list