I didn't quite know what to search for when I posted this question. It turns out that it has been (indirectly) answered across a number of other posts, which I gather here as a reference. These helped me understand what it going on (even though the why and precise how are still very mysterious):
For the following discussion the following code example is enlightening and easier than my original question:
funcs = []
for x in range(0, 3):
funcs.append(lambda: x)
for f in funcs:
print(f())
x = 5
for f in funcs:
print(f())
- 2
- 2
- 2
- 5
- 5
- 5
To blow my/your brain even more, consider that the above code example still works if you bind everything in a function:
funcs = []
def foo(i):
for x in range(i, i+3):
funcs.append(lambda: x)
foo(0)
for f in funcs:
print(f())
- 2
- 2
- 2
Even though the variable e intuitively should disappear once the function exits (meaning that even if the lambdas were looking up e in the local namespace - well, that namespace no longer exists).
While I know it won't stop future curious individuals like myself, let me state my opinion that trying to understand what's going on here is far more trouble than its worth. My conclusion from several hours of googling and reading is that I will inevitably mess up any attempt I make at storing lambdas (explicitly, or implicitly like above), and that trying to do so will result in unexpected behaviour and (incredibly) difficult behaviour to decipher.
https://www.python.org/dev/peps/pep-0227/
Why aren't python nested functions called closures?
What is a cell in the context of an interpreter or compiler?
Modify bound variables of a closure in Python
https://eev.ee/blog/2011/04/24/gotcha-python-scoping-closures/
How do lexical closures work?
What is the difference between Early and Late Binding?
http://calculist.blogspot.com/2006/05/late-and-early-binding.html
Why results of map() and list comprehension are different?
http://lambda-the-ultimate.org/node/2648
AttributeError: 'function' object has no attribute 'func_name' and python 3
http://zetcode.com/python/python-closures/
My brief (probably wildly wrong) summary of what's going on (hopefully it at least gives you a mental model with which to reason):
In the function foo above, when we create a lambda it doesn't store the value of x (like it would in e.g. SML) but rather, x
. When the lambda is later called, x
is looked up in the local namespace (like any usual variable lookup).
This is what we see in example 2. x
persists in the namespace and can be modified, which modifies future invocations of the lambdas.
Example 2 is even trickier (urgh!). Since the variable x
disappears once foo completes, one might think that the lambdas would be 'broken'. To avoid this, what python seems to do is lookahead and realise that persistent functions being created reference local variables in foo that will soon no longer be available. What it does is create a 'lexical closure' which stores the relevant local state so that the lambdas have access to the variable x
even after foo terminates.
You can see that all lambdas reference the same closure:
for f in funcs:
print(f.__closure__)
print(f.__code__.co_freevars)
(<cell at 0x10ebaa310: int object at 0x10e92bac0>,)
('x',)
(<cell at 0x10ebaa310: int object at 0x10e92bac0>,)
('x',)
(<cell at 0x10ebaa310: int object at 0x10e92bac0>,)
('x',)
which is what leads me to believe that some sort of lookahead is occurring.
Supposedly this is done to allow later modifications of the functions. I have been unable to determine whether that is only in example 1 where you still have access to the variable, or in general through e.g. some dunder attribute. There is a discussion about this in the "Modify bound variables...." link.
If you do insist on using lambdas like above, what you can do is replace
e.bind('<Double-Button-1>', lambda x: changeColor(x, e))
with
e.bind('<Double-Button-1>', lambda x, e=e: changeColor(x, e))
What's happening here is that we are giving the lambda a default value (forcing dereference of the name e
) which then leads the expected more intuitive result.
Again, just avoid the headache.