Pythonのdefaultdict()使用時にちょっと注意

RuntimeError: dictionary changed size during iteration

defaultdict をループで使ってる間に新規 key で参照してしまうとサイズが変わってエラーになる事象に以前はまり、また出会ったのでメモです。

defaultdictとは

何かの個数を辞書を使って数えたい時、Pythonのノーマル辞書では毎回keyの存在を確かめて初期化する必要がありますが、

d = {} # or d = dict()
key = "kii"
# d[key] += 1   いきなりこれはx
if key not in d:
    d[key] = 0
else:
    d[key] += 1


それをしなくて済むとても便利な defaultdict。

from collections import defaultdict

d = defaultdict(int)  # 初期値 : intなら0
key = "ki"
d[key] += 1        # いきなりok
print(d[key])      # 1
print(d["exist?"]) # 0


ノーマル辞書ほぼ使わず defaultdict を多用していますが、たまに注意が必要なことも。
まず新規 key があるか見る(参照する)だけで、なかった場合初期値をvalueに辞書内の要素(?)が増えます。

from collections import defaultdict

d = defaultdict(int)
key = "ki"
d[key] += 1
print(d[key])      # 1
print(d)           # defaultdict(<class 'int'>, {'ki': 1})
print(d["exist?"]) # 0
print(d)           # defaultdict(<class 'int'>, {'ki': 1, 'exist?': 0})


それ自体は全然良いのですが、これが for 文で辞書を使用中に新規 key で参照してしまうとエラーです。

from collections import defaultdict

d = defaultdict(int)
d['a'] += 1
d['b'] += 2

S = "abc"
for k, v in d.items():
    for s in S:
        print(s, d[s])

# a 1 defaultdict(<class 'int'>, {'a': 1, 'b': 2})
# b 2 defaultdict(<class 'int'>, {'a': 1, 'b': 2})
# c 0 defaultdict(<class 'int'>, {'a': 1, 'b': 2, 'c': 0})
# RuntimeError: dictionary changed size during iteration

for 文で使用中なので途中でサイズが変わるような操作はダメということで当たり前といえば当たり前です…。
新規 key の参照による要素数の増加だけダメなのかと思っていましたが、他にも key の削除value の更新もダメというのは出会ったことがなく調べて初めて気付きました。

これを回避するには

  • copy する
  • Counter を使う

あたり。

copy

from collections import defaultdict

d = defaultdict(int)
d['a'] += 1
d['b'] += 2

d_cp = d.copy()  # or d_cp = {**d}
S = "abc"
for k, v in d_cp.items():
    for s in S:
        print(s, d[s], d)

(↑ ↓ 使ってないですが for 文の中で key や value を使いたい想定です…)

Counter

from collections import Counter

c = Counter()
c['a'] += 1
c['b'] += 2

S = "abc"
for k, v in c.items():
    for s in S:
        print(s, c[s], c)

Counter は新規 key の参照だけでは特に要素が増えないようです。
そもそもカウント用で default = 0 と決まっているからでしょうか。

ちなみに 1

私だけかもしれないですが、0 かどうかに not を使う癖があると稀に沼ります。
個数が 0 であることと辞書に存在しないことがイコールだと思い込んで時間を溶かしたことが二度ほど…。

from collections import defaultdict
d = defaultdict(int)
d['a'] += 1
d['b'] += 2

S = "abc"
for s in S:
    print(d[s])  # {'c': 0} が増える

zero = 0
for s in S:
    if s not in d:
        zero += 1
print(zero)    # 0 である要素は1個、を期待するも 0 個
# 'c' は 0 個なので存在しないものを数えれば良い!と思い込んでいると沼


ちなみに 2

リストで試してみたらエラーにならず無限ループになりました。
これは流石に初めて…。

A = [1, 2]
for a in A:
    A.append(0)
    print(a)
# 1
# 2
# 0
# 0
# 0
# :