Какая магия предотвращает блокировку программ Tkinter в интерактивной оболочке?

Примечание. Это несколько продолжение вопроса: Tkinter - когда мне нужно позвонить mainloop?

Обычно при использовании Tkinter вы вызываете Tk. mainloop, чтобы запустить цикл событий и убедиться, что события обработаны надлежащим образом, а окна остаются интерактивными без блокировки.

При использовании Tkinter изнутри интерактивной оболочки запуск основного цикла не представляется необходимым. Возьмите этот пример:

>>> import tkinter
>>> t = tkinter.Tk()

Появится окно, и оно не будет блокировано: вы можете взаимодействовать с ним, перетаскивать его и закрывать.

Итак, что-то в интерактивной оболочке, похоже, признает, что было создано окно и запускает цикл событий в фоновом режиме.

Теперь за интересную вещь. Возьмите пример сверху, но затем в следующем приглашении (без закрытия окна) введите что угодно - без его выполнения (т.е. Не нажимайте клавишу ввода). Например:

>>> t = tkinter.Tk()
>>> print('Not pressing enter now.') # not executing this

Если вы теперь пытаетесь взаимодействовать с окном Tk, вы увидите, что он полностью блокируется. Таким образом, цикл событий, который, как мы думали, будет работать в фоновом режиме, остановился, когда мы вводили команду в интерактивную оболочку. Если мы отправим введенную команду, вы увидите, что цикл событий продолжается, и все, что мы делали во время блокировки, будет продолжать обрабатываться.

Итак, большой вопрос: Что это за магия, которая происходит в интерактивной оболочке? Что запускает основной цикл, когда мы не делаем это явно? И почему это нужно останавливать, когда мы вводим команды (вместо остановки при их выполнении)?

Примечание. Вышеупомянутое работает в интерпретаторе командной строки, а не в IDLE. Что касается IDLE, я предполагаю, что GUI обычно не говорит интерпретатору, что что-то было введено, а просто локально хранит входной файл до его выполнения.

Ответы

Ответ 1

На самом деле это неинтерактивный интерпретатор, который здесь имеет значение, но ждет ввода в TTY. Вы можете получить такое же поведение от script следующим образом:

import tkinter
t = tkinter.Tk()
input()

(В Windows вам, возможно, придется запускать script в pythonw.exe вместо python.exe, но в противном случае вам не нужно ничего особенного делать.)


Итак, как это работает? В конечном счете трюк сводится к PyOS_InputHook - так же работает модуль readline.

Если stdin является TTY, то каждый раз, когда он пытается получить строку с input(), различные биты модуля code, встроенный REPL и т.д., Python вызывает любые установленные PyOS_InputHook просто чтение из stdin.

Вероятно, легче понять то, что readline делает: он пытается select на stdin или аналогичный цикл, для каждого нового символа ввода, или каждые 0,1 секунд или любого сигнала.

То, что Tkinter делает, похоже. Это сложнее, потому что он имеет дело с Windows, но на * nix он делает что-то очень похожее на readline. За исключением того, что он вызывает Tcl_DoOneEvent каждый раз через цикл.

И это ключ. Повторное вызов Tcl_DoOneEvent - это то же самое, что mainloop.

(Threads все усложняет, конечно, но предположим, что вы не создали никаких фоновых потоков. В вашем реальном коде, если вы хотите создать фоновые потоки, у вас будет только поток для всех Tkinter материал, который блокирует на mainloop в любом случае, правильно?)


Итак, если ваш код Python тратит большую часть времени на вход TTY (как обычно это делает интерактивный интерпретатор), интерпретатор Tcl прерывается, и ваш графический интерфейс отвечает. Если вы сделаете блок интерпретатора Python чем-то другим, кроме ввода TTY, интерпретатор Tcl не будет запущен и ваш графический интерфейс не будет отвечать.


Что делать, если вы хотите сделать то же самое вручную в чистом коде Python? Вам нужно сделать это, если хотите, например, интегрировать графический интерфейс Tkinter и сетевой клиент на основе select в однопоточное приложение, верно?

Это легко: проведите один цикл от другого.

Вы можете select с таймаутом 0,02 с (тот же самый тайм-аут, который используется по умолчанию для ввода), и вызывать t.dooneevent(Tkinter.DONT_WAIT) каждый раз через цикл.

Или, в качестве альтернативы, вы можете разрешить Tk-накопителю, вызвав mainloop, но используйте after и друзей, чтобы убедиться, что вы вызываете select достаточно часто.