【问题标题】:Unexpected behavior from python tkinter double click bindpython tkinter双击绑定的意外行为
【发布时间】:2020-09-16 11:37:46
【问题描述】:

我编写了以下程序,旨在创建一个 3x3 的条目小部件网格,当双击它时,它会在白色和黑色之间切换。

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()

网格创建效果很好,但切换行为不正常。无论您双击哪个条目,它始终处于切换状态的右下方条目(循环中添加的最后一个条目)。

终端中的输出是

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

我很困惑。在绑定语句中,我们为每个条目创建一个新的专用 lambda,并传递对相关条目的引用。为什么会这样??????


我找到了解决方法,将绑定函数更改为

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

我不是要求解决我的问题,而是要求解释为什么会发生这种情况。

【问题讨论】:

  • 当 for 循环完成时,变量 e 将引用在 for 循环中创建的最后一个 Entry。因此,当稍后调用changeColor(x, e) 时,如果变量e 发生变化,e 将是最后一个Entry。使用event.widget 是更好的方法,因为 tkinter 会将其分配给触发事件的小部件。
  • 参考这个:docs.python.org/3/faq/…
  • 我实际上是在发布问题后偶然发现的。不过我还是不明白。如果您在函数内部定义循环,然后调用该函数,它仍然可以工作。当函数正在执行时,我可以购买它(创建 lambda 函数,稍后调用并在本地命名空间中查找 e,这是最近分配的值)但是,在这种情况下,一旦函数完成函数的命名空间(包括) e 消失了 - lambda 值现在在哪里查找该变量?
  • e 在全局命名空间中,不会消失。
  • 这能回答你的问题吗? how-to-understand-closure-in-a-lambda

标签: python python-3.x tkinter


【解决方案1】:

发生这种情况的原因是因为所有条目都称为e。如果您注意到,每次双击任意位置时,创建的最后一个都会更改,而不是您单击的那个。

希望这会有所帮助!

编辑:采用以下python代码:

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

输出将是:

9

同样的事情也发生在您的代码中。您正在通过迭代创建条目。因此,将生效的一个条目是最后一个创建的条目。

希望这会有所帮助!

编辑:

lambda 相同的原因是它们是相同的函数。它适用于最后一个,因为那是最后的任务。

这将是您的代码:

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()

希望这会有所帮助!

【讨论】:

  • 这不是解释。如果我们将第一个条目标记为创建 Entry1(最后一个条目为 9),当它通过 lambda x: changeColor(x, e) (e = ref to Entry1) Entry9 甚至不存在。这种变化正在反向传播。我能想到的唯一解释是,不是在每次循环迭代中创建一个新函数,而是只创建一个函数,并且该函数被一次又一次地修改。但是,我不明白为什么会这样。
  • 你好像不明白我的意思。我的意思是,当您创建条目时,最后创建的条目将是该函数适用的条目。让我编辑我的答案@EdwardGaremo
  • 我还是不明白。迭代 1:e = 0x8121.... 第一个 lambda: lambda: ....(0x8121...) 迭代 2:e = 0x2422....第二个 lambda: lambda: ....(0x2422...)如果我们将 for 循环绑定到函数 foo(): ... 那么一旦函数退出 e 就不再存在了。
  • @EdwardGaremo 对不起,我错过了!让我想想。
  • 很遗憾,您的编辑有误。如果您创建一个循环并创建一堆 lambda 函数,并将它们存储在一个列表中,并使用 id() 检查它们的内存地址,您将看到创建的不是一个而是许多函数。但是,它们确实都引用了相同的变量 e - 正如我在发布的答案中所讨论的那样。
【解决方案2】:

当我发布这个问题时,我不太清楚要搜索什么。事实证明,它已经(间接地)在许多其他帖子中得到了回答,我在这里收集这些帖子作为参考。这些帮助我了解了它发生了什么(尽管原因和确切的方式仍然非常神秘):

对于以下讨论,以下代码示例比我原来的问题更具有启发性和简单性:

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

为了让我/你的大脑更加震惊,请考虑如果您将所有内容绑定在一个函数中,上面的代码示例仍然有效:

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

即使变量 e 在函数退出后直观地应该消失(这意味着即使 lambdas 在本地命名空间中查找 e - 好吧,该命名空间不再存在)。

虽然我知道这不会阻止像我这样的未来好奇的人,但让我陈述我的观点,即试图了解这里发生的事情比它的价值要麻烦得多。我从几个小时的谷歌搜索和阅读中得出的结论是,我将不可避免地搞砸存储 lambdas 的任何尝试(明确地或隐含地像上面一样),并且尝试这样做会导致意外的行为和(难以置信的)难以处理的行为破译。

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/

我对正在发生的事情的简短(可能大错特错)总结(希望它至少能给你一个推理的心智模型):

在上面的函数 foo 中,当我们创建一个 lambda 时,它不会存储 x 的值(就像它在例如 SML 中那样),而是x。稍后调用 lambda 时,会在本地命名空间中查找 x(就像任何通常的变量查找一样)。

这就是我们在示例 2 中看到的内容。x 保留在命名空间中并且可以修改,从而修改未来对 lambda 的调用。

示例 2 更加棘手(呃!)。由于变量 x 会在 foo 完成后消失,人们可能会认为 lambda 会“损坏”。为避免这种情况,python 似乎要做的是向前看并意识到正在创建的持久函数引用 foo 中的局部变量,这些变量很快将不再可用。它所做的是创建一个“词法闭包”来存储相关的本地状态,以便 lambdas 可以访问变量 x 即使在 foo 终止之后。

你可以看到所有的 lambdas 都引用了同一个闭包:

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',)

这让我相信某种前瞻正在发生。

据说这样做是为了允许以后修改函数。我无法确定这是否仅在示例 1 中您仍然可以访问该变量,或者通常通过例如一些dunder属性。在“修改绑定变量......”链接中有关于此的讨论。

如果你坚持使用上面的 lambda,你可以做的就是替换

e.bind('<Double-Button-1>', lambda x: changeColor(x, e))

e.bind('<Double-Button-1>', lambda x, e=e: changeColor(x, e))

这里发生的是我们给 lambda 一个默认值(强制取消引用名称 e),然后导致预期的更直观的结果。

再次,避免头痛。

【讨论】:

    猜你喜欢
    • 2020-11-27
    • 2011-04-27
    • 2015-02-02
    • 1970-01-01
    • 2013-04-05
    • 1970-01-01
    • 1970-01-01
    • 2020-06-13
    相关资源
    最近更新 更多