Глава 16 · Создаём классные приложения с Tkinter

after(): таймеры без блокировки

Нужно подождать — не значит нужно остановить всё приложение.

after() — отложенный вызов без блокировки

after_prosto.py
after_id = root.after(1000, lambda: print("Прошла секунда"))
# программа продолжает обрабатывать другие события, ожидая эту секунду
событийный цикл
запланировать через after()
другие события продолжают обрабатываться
callback вызывается
время подошло
after() ставит callback в очередь на будущее — и не блокирует всё остальное в ожидании.
«Через секунду» — не хронометр с точностью до миллисекунды
after(1000, callback) означает «сделай callback готовым к выполнению примерно через 1000 мс, когда событийный цикл сможет его обработать» — не гарантию исполнения ровно в указанную миллисекунду. Если в этот момент цикл занят другим событием, вызов произойдёт чуть позже.

Повторяющийся таймер (self-rescheduling)

tik.py
after_id = None

def tik():
    global after_id
    label.config(text=f"Секунд: {schet.get()}")
    schet.set(schet.get() + 1)
    after_id = root.after(1000, tik)   # сам себя планирует заново — вот и повторение
Не только for/while — это тоже повторение
Из главы 10 мы знаем циклы for/while. Повторяющийся вызов через self-scheduling after() — ещё один способ организовать повторение, устроенный на событиях, а не на последовательном блоке кода.
Каждый новый after() должен обновлять сохранённый id
У tik() — новый вызов after() на каждом тике, и, значит, новый идентификатор. Если не переприсваивать after_id заново при каждом вызове (как выше), сохранённая переменная быстро устареет — будет указывать на уже сработавший вызов, а не на актуально ожидающий, и попытка остановить таймер по этому id ничего не остановит.

Остановка таймера: after_cancel

after_cancel.py
def stop():
    global after_id
    if after_id is not None:
        root.after_cancel(after_id)
        after_id = None
Не потеряйте идентификатор
Чтобы остановить запланированный вызов, нужно сохранить актуальный идентификатор, который вернул последний вызов after(). Без него отменить именно этот запланированный вызов не получится. Полную трёхсостояньевую модель (остановлен/идёт/завершён) с корректным after_id смотрите в мини-проекте «Таймер» (раздел 16.28).
Debug Lab 3: Повторный запуск создаёт дублирующиеся таймеры
duplicate_after.py
def start():
    tik()   # каждый клик по «Старт» запускает ЕЩЁ один цикл tik()

ttk.Button(root, text="Старт", command=start).pack()
# После нескольких нажатий «Старт» счётчик начинает прыгать сразу на 2, 3, 4 за секунду —
# работает уже несколько параллельных цепочек after().
Что видно на экране

Каждый клик запускает новую независимую цепочку самопланирующихся вызовов tik(), и они не заменяют друг друга, а работают одновременно. Нужно блокировать повторный старт через общий флаг running — и этот же флаг должна проверять сама tik(), иначе цепочка не остановится даже после stop().

Исправленный код
duplicate_after_fixed.py
running = False
after_id = None

def tik():
    global after_id, running
    if not running:
        return   # stop() уже сбросил флаг — новую цепочку не планируем
    label.config(text=f"Секунд: {schet.get()}")
    schet.set(schet.get() + 1)
    after_id = root.after(1000, tik)

def start():
    global running
    if running:
        return   # уже запущено — повторный клик игнорируем
    running = True
    tik()

def stop():
    global running, after_id
    running = False
    if after_id is not None:
        root.after_cancel(after_id)
        after_id = None
Никогда time.sleep() внутри callback
time.sleep(...) в обработчике останавливает событийный цикл целиком на всё время сна — интерфейс перестаёт отвечать. Подробный разбор — в разделе 16.32.
Практика: таймер обратного отсчёта через after()
Модуль tkinter открывает нативное окно Python — выполните локально в VS Code, PyCharm или Jupyter
Практика выполняется локально
Открыть практику →