辞書をイテレート中に変更した例外の原因と直し方
辞書をイテレート中にキーを追加・削除すると発生する例外です。リストと違い、辞書はサイズの変化を検出して例外を投げます。
エラーメッセージの読み方
Traceback (most recent call last):
main.py- ファイル名
2- 行番号 — 実際にクラッシュした行
<module>- 発生場所 — <module> ならトップレベル、関数名ならその関数の中
RuntimeError- 例外クラス — 何が起きたか。ここを検索するのが最短です
dictionary changed size during iteration- 詳細メッセージ — どの値が問題だったか
このエラーが出る典型パターン
パターン1
1 scores = {"a": 1, "b": 0, "c": 3} 2 for key in scores : ^ 3 if scores[key] == 0: 4 del scores[key] 5 print(scores)
辞書はイテレート中にサイズが変わることを検出すると例外を投げます。安全に削除するには、先にキーのコピー(list())を作ってそちらをイテレートします。
直し方: scores を list(scores) にします。
パターン2
1 inventory = {"apple": 5, "banana": 0, "cherry": 2} 2 for item in inventory : ^ 3 if inventory[item] == 0: 4 del inventory[item] 5 print(inventory)
在庫が0の商品を削除するような処理でよく起きます。辞書のキー一覧を先にlist()でコピーしておけば安全です。
直し方: inventory を list(inventory) にします。
パターン3
1 settings = {"a": True, "b": False, "c": True} 2 for key in settings : ^ 3 if not settings[key]: 4 del settings[key] 5 print(settings)
真偽値フラグの整理でも同じです。Falseの項目を削除する処理はよく書かれるパターンなので、覚えておくと役に立ちます。
直し方: settings を list(settings) にします。
パターン4
1 counts = {"x": 3, "y": 0, "z": 1} 2 for key in counts : ^ 3 if counts[key] == 0: 4 del counts[key] 5 print(counts)
件数が0のキーを取り除く処理でも同じ例外になります。先にキーの一覧をlist()でコピーしてからイテレートします。
直し方: counts を list(counts) にします。
パターン5
1 flags = {"debug": 0, "verbose": 1, "cache": 0} 2 for name in flags : ^ 3 if flags[name] == 0: 4 del flags[name] 5 print(flags)
オフになっているフラグを削除する処理でも同様です。辞書は自身のサイズ変化を検出して例外を投げます。
直し方: flags を list(flags) にします。
よくある誤解
リストの要素削除は例外にならず静かに壊れますが、辞書は検出して例外を投げてくれます。この挙動の違いを知らないと混乱します。
実務での勘所
辞書がこの変化を検出できるのは、内部で変更回数を記録しており、イテレート中にその回数が変わっていないかを毎回確認しているためです(Javaのfail-fast Iteratorと同じ考え方の仕組みです)。安全に辞書を変更しながら処理したい場合は、for key in list(d.keys()):のように、キーの一覧を先にリストとしてコピーしてから、そのコピーに対してループしつつ元の辞書を変更します。コピーはループ開始時点のスナップショットなので、元の辞書をいくら変更してもコピー自体は影響を受けません。