辞書をa | bで結合すると、同じキーの値は右側の辞書の値に置き換わる。数値だから自動的に足されるわけではない。集計結果をまとめる場合は、結合前にキーの重なりを調べ、上書きを許可する・重複をエラーにする・数を足すという方針を選んでおく。値が消えたように見える問題の多くは、この方針が曖昧なまま結合したことから起こる。
まず上書きされるキーを見つける
ここでは二つの測定日の試料別件数を一つにまとめる。Aは最初の日だけ、Cは次の日だけに存在し、Bは両方に存在する。このBをどう扱うかで正しい結果が変わるため、単に短い式で書けるかどうかでは結合方法を決められない。
辞書のkeys()が返すビューには集合演算を使える。left.keys() & right.keys()で共通キーを求められるので、上書きされる項目を事前に一覧にできる。例ではキーを文字列に統一し、表示を安定させるためsorted()で並べている。
三つの結合方針を実装する
example.py
left = {"A": 2, "B": 3}
right = {"B": 5, "C": 4}
print("conflicts:", sorted(left.keys() & right.keys()))
print("overwrite:", left | right)
def strict_merge(a, b):
conflicts = a.keys() & b.keys()
if conflicts:
raise ValueError(f"duplicate keys: {sorted(conflicts)}")
return a | b
try:
strict_merge(left, right)
except ValueError as exc:
print(type(exc).__name__ + ":", exc)
else:
raise AssertionError("The duplicate key must be rejected")
def sum_merge(a, b):
result = a.copy()
for key, value in b.items():
result[key] = result.get(key, 0) + value
return result
merged = sum_merge(left, right)
print("sum:", merged)
assert merged == {"A": 2, "B": 8, "C": 4}
assert left == {"A": 2, "B": 3}
assert strict_merge({"A": 1}, {"B": 2}) == {"A": 1, "B": 2}
assert sum(merged.values()) == sum(left.values()) + sum(right.values())
実行結果
conflicts: ['B']
overwrite: {'A': 2, 'B': 5, 'C': 4}
ValueError: duplicate keys: ['B']
sum: {'A': 2, 'B': 8, 'C': 4}
最初の結果ではBが3から5へ置き換わる。strict_mergeは、重複が一つでもあれば新しい辞書を返さずValueErrorにする。sum_mergeは左の辞書をコピーしてから右を足すので、Bは8になり、もとの辞書は書き換わらない。
重複エラーと合算を使い分ける
IDごとに一つのレコードしか存在してはいけないデータなら、重複をエラーにする方法が向いている。後から上書きしてしまうと、どちらのデータが消えたかを調べにくい。検出した時点で止めれば、入力ファイルや集計の単位が誤っていないかを確認できる。
一方、同じ試料の件数や積算値をまとめるなら、値を加算する方法が自然である。ただし、この例のsum_mergeは数値の加算を前提とする。文字列なら連結、リストならリスト結合になるため、想定外の型を入れないよう呼び出し側で検査する。異なる単位の値もそのまま足してはいけない。
設定ファイルのように、後から指定した値を優先する約束がある場合は上書きが正しい。例えば既定のtimeoutを今回だけ変更する用途なら、defaults | overridesという順序を明示すればよい。左右を逆にすると優先順位も逆になる。
入れ子の辞書は自動で深く結合されない
通常の辞書結合は、最上位のキーについて行われる。scanというキーの下に別の辞書がある場合、scanの中の一項目だけが追加されるのではなく、その値である辞書全体が右側のものへ置き換わる。深い結合が必要なら、項目ごとのルールを別途決める必要がある。
また、a | bで新しい辞書ができても、そこに格納された内側のリストや辞書まで独立するわけではない。結合後のリストへ追加して元まで変わる場合は、結合の優先順位ではなく共有参照の問題である。関連する浅いコピーの記事と併せて確認すると原因を切り分けやすい。
上書きを意図した処理に見せる
|演算子と|=はPython 3.9以降で使える。a |= bやa.update(b)は左側の辞書そのものを更新するため、他の変数も同じ辞書を参照している場合は変更が共有される。元を残したい処理では、新しい辞書を返す実装を選ぶ方が追跡しやすい。
辞書は挿入順序を保持するが、既存キーを上書きしただけではそのキーが末尾へ移るわけではない。結果の表示順まで仕様に含めるなら、結合と並べ替えを別の処理として考える。キーの欠落、値の衝突、表示順を分けて確かめると、集計の確認がしやすくなる。
結合前の辞書を保存し、共通キーの一覧と選んだ処理方針を確認するだけでも、黙って値を失う事故を減らせる。件数の合算では、結合前後の合計が一致するかも確認するとよい。ただし上書きや重複排除を行う用途では、合計が一致しないこと自体は誤りではない。
動作確認と参考資料
掲載例はLinux・CPython 3.12.14で動作確認した。OS固有のコマンドや環境ごとに変わるパスは、本文中の条件を確認して使ってほしい。
