静かなる名辞

pythonとプログラミングのこと



ループで辞書の要素を削除しようと思ったらRuntimeError: dictionary changed size during iteration

前提

  • ループで条件に従って辞書の全要素を舐め、条件が真になる要素を削除したい
  • あくまでもin-placeで処理したい(今回はdel文で書いていた)

 要するにこんなコード。

d = {v:"hoge!"*v for v in range(5)}
# => {0: '', 1: 'hoge!', 2: 'hoge!hoge!', 3: 'hoge!hoge!hoge!', 4: 'hoge!hoge!hoge!hoge!'}
for k in d.keys():
    if len(d[k]) > 10: 
        del d[k]

 そしてこうなる。

RuntimeError: dictionary changed size during iteration

原因

 dict.keys()でループ中に辞書を変更してはいけない。

 dict.keys()は辞書ビューオブジェクトと言い、他の言語でいうところのジェネレータのようなものである(pythonはジェネレータの定義が特殊なので単にジェネレータと呼ぶのは憚られるけど)。それだけならともかく、dict.keys()の中身は動的に辞書の中身を反映し、よって辞書の中身を変えながらdict.keys()を見ることはできない。

辞書の項目の追加や削除中にビューをイテレートすると、 RuntimeError を送出したり、すべての項目に渡ってイテレートできなかったりします。

https://docs.python.jp/3/library/stdtypes.html#dict-views

 他に、dict.items()やdict.values()等でループしても同じエラーを拝めるはずである。

対策

 他の言語でいうところのジェネレータになっているのが悪いので、ループ前にlistに変換してやる。

d = {v:"hoge!"*v for v in range(5)}
for k in list(d.keys()):
    if len(d[k]) > 10: 
        del d[k]
print(d)  # => {0: '', 1: 'hoge!', 2: 'hoge!hoge!'}

 dict.values()やdict.items()でループする場合も同じ対処でいけます。

 めでたしめでたし。