-2

I wrote the following program intended to create a 3x3 grid of entry widgets which, when double clicked, toggle between white and black.

from tkinter import *

root = Tk()

temp = None

def changeColor(event, e):
    print("id(e) =", id(e))
    if e['bg'] == 'white':
        e['bg'] = 'black'
    elif e['bg'] == 'black':
        e['bg'] = 'white'
    global temp
    temp = event

entries = [[None for i in range(3)] for j in range(3)]

for y in range(3):
    for x in range(3):
        e = Entry(root, width=3, bg='white', bd=0, borderwidth=3)
        e.bind('<Double-Button-1>', lambda x: changeColor(x, e))
        e.grid(column=x, row=y)
        entries[y][x] = e

root.mainloop()

The grid creation works great but the toggling is misbehaving. Regardless of which of the entries you double click, its always the bottom right entry (the last one added in the loop) which is toggled.

The output in the terminal is

id(e) = 4376431536
id(e) = 4376431536
id(e) = 4376431536
...

I'm very confused. In the bind statements we create a new specialised lambda for each of the entries and pass a reference to the relevant entry. Why is this happening??????


I found a work around, changing the bind function to

def changeColor(event, e):
    e2 = event.widget
    if e2['bg'] == 'white':
        e2['bg'] = 'black'
    elif e2['bg'] == 'black':
        e2['bg'] = 'white'

I'm not asking for a solution to my issue, but asking for an explanation of why it is happening.

Edward Garemo
  • 434
  • 3
  • 13
  • When the for loops completed, variable `e` will be a reference to the last `Entry` created in the for loop. So when `changeColor(x, e)` is called later, `e` will be the last `Entry` if there is on change on the variable `e`. Using `event.widget` is the better way as tkinter will assign it to the widget that triggers the event. – acw1668 May 29 '20 at 02:53
  • Refert to this:https://docs.python.org/3/faq/programming.html?highlight=lambda#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result – jizhihaoSAMA May 29 '20 at 03:05
  • I actually stumbled across that after I posted the question. I still don't understand though. If you define the loops inside of a function and then call that function it still works. While the function is executing I can buy it (the lambda function is created, later called and looks up e in the local namespace which is the most recently assigned value) but, in this case, once the function finishes the function's namespace (including) e disappears - where does the lambda value now look up that variable? – Edward Garemo May 29 '20 at 07:30
  • 1
    `e` is in global namespace, it won't disappear. – acw1668 May 29 '20 at 07:32
  • Does this answer your question? [how-to-understand-closure-in-a-lambda](https://stackoverflow.com/questions/28494089) – stovfl May 29 '20 at 09:13

2 Answers2

-1

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.

Edward Garemo
  • 434
  • 3
  • 13
-2

The reason it is happening is because all of the entries are called e. If you notice, everytime you double click anywhere, the final one that is created changes, instead of the one you click on.

Hope this helps!

Edit: Take the following python code:

loop = 0
for i in range(10):
    loop = i
print(loop)

The output would be:

9

The same thing is happening in your code. You are creating the entry's through iteration. Therefore, the one entry that will be in effect is the last one created.

Hope this helps!

Edit:

The reason the lambda's are the same is because they are the same function. It applies to the last one because that is the final assignment.

This would be your code:

from tkinter import *

root = Tk()

temp = None

def changeColor(event):
    if event.widget['bg'] == 'white':
        event.widget['bg'] = 'black'
    elif event.widget['bg'] == 'black':
        event.widget['bg'] = 'white'
    global temp
    temp = event

entries = [[None for i in range(3)] for j in range(3)]

for y in range(3):
    for x in range(3):
        e = Entry(root, width=3, bg='white', borderwidth=3)
        e.bind('<Double-Button-1>', lambda x: changeColor(x))
        e.grid(column=x, row=y)
        entries[y][x] = e

root.mainloop()

Hope this helps!

10 Rep
  • 2,217
  • 7
  • 19
  • 33
  • This is not an explanation. If we label the first entry created Entry1 (and the last one 9), when it passes lambda x: changeColor(x, e) (e = ref to Entry1) Entry9 doesn't even exist. The change is backpropagating. The only explanation I can think of is that instead of creating a new function on each loop iteration only one function is created, and that function is modified again and again. But, I don't understand why this would be the case. – Edward Garemo May 29 '20 at 01:42
  • You don't seem to understand me. What I mean is when you create the entry's, the one that is created last would be the entry that the function applies to. Let me edit my Answer @EdwardGaremo – 10 Rep May 29 '20 at 01:44
  • I still don't understand. Iteration 1: e = 0x8121.... 1st lambda: lambda: ....(0x8121...) Iteration 2: e = 0x2422.... 2nd lambda: lambda: ....(0x2422...) If we bind the for loop in a function foo(): ... then once the function exits e ceases to exist. – Edward Garemo May 29 '20 at 01:52
  • @EdwardGaremo Sorry, I missed this! Let me think. – 10 Rep May 29 '20 at 01:54
  • Your edit is wrong unfortunately. If you create a loop and create a bunch of lambda functions, and store those in a list, and examine their memory addresses with id() you'll see that not one but many functions are created. They DO however all refer to the same variable e - as I discuss in the answer I posted. – Edward Garemo May 31 '20 at 12:52
  • @EdwardGaremo Yes, multiple functions are created, I agree. However, that is neccesary: there is no other way to make it. – 10 Rep May 31 '20 at 20:50
  • In your edit you say, "The reason the lambda's are the same is because they are the same function" - this is not correct. They are not the same function, there are different identical functions. – Edward Garemo May 31 '20 at 22:16
  • @EdwardGaremo Then why is their id the same? – 10 Rep May 31 '20 at 22:16
  • They are not. Run: >>> funcs = [] >>> for x in range(3): ... funcs.append(lambda: print(x)) ... >>> funcs[0]() 2 >>> funcs[1]() 2 >>> funcs[2]() 2 >>> id(funcs[0]) 4429999984 >>> id(funcs[1]) 4430856832 >>> id(funcs[2]) 4434077296 – Edward Garemo May 31 '20 at 22:22
  • @EdwardGaremo Please specify what is your question. It seems you know why it doesn't work, so what do you want? – 10 Rep May 31 '20 at 22:23
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/215052/discussion-between-edward-garemo-and-themaker). – Edward Garemo May 31 '20 at 22:32