前提
- ループで条件に従って辞書の全要素を舐め、条件が真になる要素を削除したい
- あくまでも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 を送出したり、すべての項目に渡ってイテレートできなかったりします。
他に、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()でループする場合も同じ対処でいけます。
めでたしめでたし。