Code Fix

上級

辞書をイテレート中に変更した例外の原因と直し方

辞書をイテレート中にキーを追加・削除すると発生する例外です。リストと違い、辞書はサイズの変化を検出して例外を投げます。

エラーメッセージの読み方

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)
Traceback (most recent call last): File "main.py", line 2, in <module> for key in scores: ^^^^^^ RuntimeError: dictionary changed size during iteration

辞書はイテレート中にサイズが変わることを検出すると例外を投げます。安全に削除するには、先にキーのコピー(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)
Traceback (most recent call last): File "main.py", line 2, in <module> for item in inventory: ^^^^^^^^^ RuntimeError: dictionary changed size during iteration

在庫が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)
Traceback (most recent call last): File "main.py", line 2, in <module> for key in settings: ^^^^^^^^ RuntimeError: dictionary changed size during iteration

真偽値フラグの整理でも同じです。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)
Traceback (most recent call last): File "main.py", line 2, in <module> for key in counts: ^^^^^^ RuntimeError: dictionary changed size during iteration

件数が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)
Traceback (most recent call last): File "main.py", line 2, in <module> for name in flags: ^^^^^ RuntimeError: dictionary changed size during iteration

オフになっているフラグを削除する処理でも同様です。辞書は自身のサイズ変化を検出して例外を投げます。

直し方: flags を list(flags) にします。

この問題を解いてみる →

よくある誤解

リストの要素削除は例外にならず静かに壊れますが、辞書は検出して例外を投げてくれます。この挙動の違いを知らないと混乱します。

実務での勘所

辞書がこの変化を検出できるのは、内部で変更回数を記録しており、イテレート中にその回数が変わっていないかを毎回確認しているためです(Javaのfail-fast Iteratorと同じ考え方の仕組みです)。安全に辞書を変更しながら処理したい場合は、for key in list(d.keys()):のように、キーの一覧を先にリストとしてコピーしてから、そのコピーに対してループしつつ元の辞書を変更します。コピーはループ開始時点のスナップショットなので、元の辞書をいくら変更してもコピー自体は影響を受けません。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)