【Python】Tkinterの update() / update_idletasks() の違いと注意点

# 1. はじめに

Tkinterに存在する `update()` と `update_idletasks()` について記載する。

# 2. 用途・機能・例

## `update()`

`tkinter\__init__.py` にある `update()` のdocstring:

> Enter event loop until all pending events have been processed by Tcl.

Google Translate 和訳:

> すべての保留中のイベントが Tcl によって処理されるまで、イベント ループに入ります。

## `update_idletasks()`

`tkinter\__init__.py` にある `update_idletasks()` のdocstring:

> Enter event loop until all idle callbacks have been called. This will update the display of windows but not process events caused by the user.

Google Translate 和訳:

> すべてのアイドルコールバックが呼び出されるまでイベントループに入ります。これにより、ウィンドウの表示は更新されますが、ユーザーによって発生したイベントは処理されません。

処理を比較すると下記のようになる。

|関数|処理|
|:-:|:-:|
|`update()`|画面の描画、ユーザの操作、イベント処理を即時反映する。<br>`mainloop()` を1回手動で呼び出す。|
|`update_idletasks()`|アイドル状態である画面の描画のみ即時反映する。|

通常、画面の描画やイベント処理などは、イベントループである `mainloop()` で行う。

ただ、大量のウィジェットを表示したいような場合、`mainloop()` に辿り着いてから一気に処理するのではなく、段階的に表示したいということがある。このような場合に、`update_idletasks()` を使う。実装例は下記のコードのようになる。

また、ユーザの操作などは `update_idletasks()` では処理しないため、処理したい場合は `update()` を使う。

```python:main.py
import tkinter as tk
from tkinter import ttk


if __name__ == '__main__':
    root = tk.Tk()

    num_x_buttons = 10
    num_y_buttons = 10

    root.grid_rowconfigure(tuple(range(num_y_buttons)), weight=1)
    root.grid_columnconfigure(tuple(range(num_x_buttons)), weight=1)

    for y in range(num_y_buttons):
        for x in range(num_x_buttons):
            button = ttk.Button(root, text=f'({x}, {y})')
            button.grid(row=y, column=x, sticky=tk.NSEW)
        root.update_idletasks()  # 1行ずつ表示

    root.mainloop()
```

# 3. 注意点

原則、Tkinterでは `mainloop()` を呼び出してイベントループを実行する。一方、`update()` や `update_idletasks()` は、手動で強制的かつ一時的にイベントループに介入する。そのため、`update()` や `update_idletasks()` を使用することで、イベントが正常に機能しない場合がある。処理内容を比較すると、`update_idletasks()` のほうが `update()` よりリスクは低いが、基本的には全て `mainloop()` に処理させる。 

`time.sleep()` など時間のかかる処理をすることで、描画が即座に行われない。この対策として`update()` を使っているのをネットで散見するが、このような対処方法は不適切である。指定時間後に処理を行いたければ `after()` を使う。時間のかかる処理を伴う場合は、サブスレッドで時間のかかる処理を行い、必要に応じてキューを使ったポーリングを実装する。このような対応が適切である。

キューを使ったポーリングの実装方法:
[【Python】Tkinterで画面が固まる原因と解決方法](https://qiita.com/ab-boy_ringo/items/3d6cc130c252f3652698)

# 4. まとめ

`update()` と `update_idletasks()` はどちらも更新処理を行う関数だが、原則使わない（`mainloop()` で処理する）ことを推奨する。もし、これらの関数を使っているのであれば、使わないようにコードを見直す必要があるかもしれない。

# 5. おまけ

個人的に試してみたいことがいくつかあったため、一応コードを載せておく。

**検証環境**
OS: Windows 11 Pro 24H2
Python: 3.13.3

## `update()` と `update_idletasks()` の処理の違いを検証するコード

:::note warn
検証のために `sleep()` を入れているが、実際のアプリケーションではメインスレッドに時間のかかる処理を記述してはならない。
:::

```python:main.py
import time
import tkinter as tk
from tkinter import ttk


if __name__ == '__main__':
    root = tk.Tk()

    for i in range(10):
        ttk.Button(root, text=f'Button{i}', command=lambda n=i: print(n)).pack(pady=5)
        time.sleep(1)  # 検証のためsleepを入れているが、本来は入れてはならない
        # root.update()
        # root.update_idletasks()

    root.mainloop()
```

`update()` と `update_idletasks()` を両方無効にした場合、メインスレッド内に時間のかかる処理（`time.sleep(1)`）があるため、イベントループが呼び出されるまでに時間がかかっている。

`update()` を有効にした場合、ボタンが1つずつ1秒ごとに描画されていく。ボタンを押したときのコールバック関数も1秒ごとに処理している。

`update_idletasks()` を有効にした場合、ボタンが1つずつ1秒ごとに描画されていくが、ウィンドウが一部黒くなるなど期待と異なる外観になる。また、クリックイベントは受け付けず、応答なしになる。

（動作結果は実行環境によって異なる可能性あり。）

## `update()` で `mainloop()` を再現

:::note alert
正常な振る舞いをしていたとしても、同じ方法で実装してしまうとバグが発生する原因となるため、実際のアプリケーションでは `mainloop()` を使うこと。
:::

`mainloop()` と同じ処理を1度だけするのが `update()` ならば、ループさせれば同じ挙動になるか興味があったので検証。

```python:main.py
import time
import tkinter as tk
from tkinter import ttk


def on_button():
    # after 検証用
    root.after(1000, print, 'after1')
    root.after(2000, print, 'after2')
    root.after(3000, print, 'after3')


def on_close_button():
    global is_loop
    is_loop = False
    root.destroy()


if __name__ == '__main__':
    is_loop = True

    root = tk.Tk()

    button = ttk.Button(root, text='Button', command=on_button)
    button.pack()

    root.protocol('WM_DELETE_WINDOW', on_close_button)

    # キーイベント検証用
    root.bind('<Key>', lambda e: print(e))

    # mainloop() と置き換え可能か検証
    while is_loop:
        root.update()
        time.sleep(0.001)  # 負荷を減らすためのsleepだが、ループを常時実行していれば結局高負荷のまま
```

検証環境では、`mainloop()` を使った時と同じような振る舞いをした。ただし、`update()` の無限ループは、無駄にCPUを使用し続けたり、イベントが正常に処理されない可能性があるなど問題点はいくつかある。

ちなみに `update()` を `update_idletasks()` に変えると、ウィジェットの描画がされず、操作もできない状態となった。