Неожиданное поведение при привязке двойного щелчка python tkinter - PullRequest
0 голосов
/ 29 мая 2020

Я написал следующую программу, предназначенную для создания сетки виджетов 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()

Создание сетки работает отлично, но переключение некорректно. Независимо от того, на какой из записей вы дважды щелкните, всегда будет переключаться нижняя правая запись (последняя добавленная в l oop).

Вывод в терминале

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

Я очень запутался. В операторах связывания мы создаем новую специализированную лямбду для каждой записи и передаем ссылку на соответствующую запись. Почему это происходит ??????


Я нашел работу, изменив функцию привязки на

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

Я не прошу решения моей проблемы , но просят объяснить, почему это происходит.

Ответы [ 2 ]

0 голосов
/ 29 мая 2020

Я не совсем знал, что искать, когда разместил этот вопрос. Оказывается, на него (косвенно) ответили в ряде других сообщений, которые я собираю здесь в качестве справки. Это помогло мне понять, что происходит (даже несмотря на то, что «почему» и «как именно» все еще очень загадочны):

Для следующего обсуждения следующий пример кода поучительнее и проще, чем мой исходный вопрос:

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 интуитивно должна исчезнуть после выхода из функции (это означает, что даже если лямбда-выражения искали e в локальном пространстве имен - ну, это пространство имен больше не существует).

Хотя я знаю, что это не остановит будущих любопытных людей, таких как я, позвольте мне высказать свое мнение о том, что попытка понять, что здесь происходит, - гораздо больше проблем, чем того стоит. Мой вывод из нескольких часов поиска в Google и чтения заключается в том, что я неизбежно испорчу любую попытку сохранить лямбды (явно или неявно, как указано выше), и что попытка сделать это приведет к неожиданному поведению и (невероятно) сложному поведению для расшифровать.

https://www.python.org/dev/peps/pep-0227/

Почему python вложенные функции не называются замыканиями?

Что такое ячейка в контексте интерпретатора или компилятора?

Изменить связанные переменные замыкания в Python

https://eev.ee/blog/2011/04/24/gotcha-python-scoping-closures/

Как работают лексические замыкания?

В чем разница между ранним и поздним связыванием?

http://calculist.blogspot.com/2006/05/late-and-early-binding.html

Почему результаты map () и понимания списка различаются?

http://lambda-the-ultimate.org/node/2648

AttributeError: объект 'function' не имеет атрибута 'func_name' и python 3

http://zetcode.com/python/python-closures/

My bri ef (возможно, совершенно неверно) краткое изложение того, что происходит (надеюсь, это, по крайней мере, дает вам мысленную модель для рассуждений):

В приведенной выше функции foo, когда мы создаем лямбду, она не сохраняет значение x (как, например, в SML), а скорее x. Когда позже вызывается лямбда, x ищется в локальном пространстве имен (как и любой обычный поиск переменных).

Это то, что мы видим в примере 2. x сохраняется в пространстве имен и может быть изменено, что изменяет будущие вызовы лямбда-выражений.

Пример 2 еще сложнее (ура!) . Поскольку переменная x исчезает после завершения foo, можно подумать, что лямбда-выражения будут «сломаны». Чтобы избежать этого, python, похоже, смотрит вперед и понимает, что создаваемые постоянные функции ссылаются на локальные переменные в foo, которые скоро больше не будут доступны. Что он делает, так это создает «лексическое замыкание», которое сохраняет соответствующее локальное состояние, так что лямбда-выражения имеют доступ к переменной x даже после завершения foo.

Вы можете видеть, что все лямбда-выражения ссылаются на одно и то же закрытие:

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. Это обсуждается в ссылке «Изменить связанные переменные ....».

Если вы все же настаиваете на использовании лямбда-выражений, как указано выше, вы можете заменить

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

на

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

Здесь происходит то, что мы даем лямбда - значение по умолчанию (принудительное разыменование имени e), которое затем приводит к ожидаемому более интуитивному результату.

Опять же, просто избегайте головной боли.

0 голосов
/ 29 мая 2020

Причина, по которой это происходит, в том, что все записи называются e. Если вы заметили, каждый раз, когда вы дважды щелкаете в любом месте, последний созданный изменяется, а не тот, который вы щелкаете.

Надеюсь, это поможет!

Изменить: возьмите следующий код python:

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

Результат будет:

9

то же самое происходит в вашем коде. Вы создаете запись путем итерации. Следовательно, будет действовать одна запись, созданная последней.

Надеюсь, это поможет!

Изменить:

Причина, по которой лямбда одинаковы, в том, что это одна и та же функция. Это относится к последнему, потому что это последнее задание.

Это будет ваш код:

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

Надеюсь, это поможет!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...