- Python
Python辞書の結合・マージ updateと|演算子とsetdefault
Pythonの辞書の結合(マージ)とは、2つ以上の辞書のキーと値を1つにまとめる操作で、元の辞書を書き換えるなら update か |=、新しい辞書を作るなら | 演算子か {**a, **b} を使います。 どの書き方でも、同じキーが両方にあるときは「後から渡したほう(右側)」の値が残ります。既にある値を残したいときは setdefault、辞書をコピーせずに重ねて見たいときは ChainMap が合います。
この記事では、6つの方法を 「元の辞書を書き換えるか」「キー重複時にどちらが残るか」「対応バージョン」 の3つの軸で比較します。すべてのコードは Python 3.11.15 と 3.14.7 で実行し、出力をそのまま載せました(| 演算子は 3.8.20 と 3.10.18 でも実行しています)。ネストした辞書が丸ごと置き換わる様子、コピーして使える再帰マージ関数、上書きしない結合、リスト値を足し合わせる結合まで、ひととおり確認できます。
辞書の結合・マージ方法の早見表
まずは結論の表です。迷ったら「新しい辞書がほしい → a | b」「a 自体を更新したい → a.update(b)」 と覚えておくと、ほとんどの場面で用が足ります。
| 書き方 | 元の辞書 a | キー重複時に残る値 | 戻り値 | 対応バージョン | 向いている場面 |
|---|---|---|---|---|---|
a.update(b) |
書き換える | b(後から渡した側) | None |
すべての Python 3 | a をその場で更新する |
a | b |
そのまま | b(右側) | 新しい辞書 | 3.9 以降 | 新しい辞書を1行で作る |
a |= b |
書き換える | b(右側) | (代入文) | 3.9 以降 | update を演算子で書く |
{**a, **b} |
そのまま | b(後ろ側) | 新しい辞書 | 3.5 以降 | 3.8 以前でも動かす、キーを足しながら結合する |
a.setdefault(k, v) |
キーがないときだけ書き換える | a(既にある値) | 最終的な値 | すべての Python 3 | 上書きせずに初期値を入れる |
ChainMap(a, b) |
そのまま(コピーしない) | a(先に渡した側) | ChainMap | 3.3 以降 | 設定の優先順位を重ねて参照する |
やりたいことから探すときは、次の逆引き表を使ってください。
| やりたいこと | 書き方 | 結果のポイント |
|---|---|---|
| 2つの辞書から新しい辞書を作る | c = a | b |
a も b も変わらない |
| a に b の内容を取り込む | a.update(b) |
a が変わる。戻り値は None |
| 3つ以上をまとめる | a | b | c または {**a, **b, **c} |
右にあるものほど優先 |
| 既にあるキーは上書きしない | defaults | user または setdefault のループ |
優先したい辞書を右に置く |
| ネストした辞書も中身ごとマージする | 再帰マージ関数(後述の deep_merge) |
標準の方法はすべて浅いマージ |
| リストの値を連結する | out.setdefault(k, []).extend(v) |
上書きではなく足し合わせる |
| 数値を合計する | Counter(a) + Counter(b) |
0 以下の値は消える点に注意 |
| コピーせずに優先順位つきで参照する | ChainMap(user, defaults) |
元の辞書の変更が反映される |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python辞書の結合・マージ updateと|演算子とsetdefault」
確認に使った環境と入力データ
この記事の実行結果は、次の環境で得たものです。
| 項目 | 内容 |
|---|---|
| 主な実行環境 | Python 3.11.15(Linux) |
| 追加で実行した版 | Python 3.14.7(全コード、出力は 3.11 と同じ)、3.10.18 と 3.8.20(| 演算子の対応確認) |
| 使うモジュール | 標準ライブラリのみ(collections、copy、functools) |
多くの例で、次の2つの辞書を使います。age というキーが両方にある点がポイントです。
a = {"name": "taro", "age": 20}
b = {"age": 21, "city": "Tokyo"}
updateで元の辞書を書き換えて結合する
a.update(b) は、a 自体に b のキーと値を取り込みます。 同じキーがあれば b の値で上書きされ、b は変わりません。
a = {"name": "taro", "age": 20}
b = {"age": 21, "city": "Tokyo"}
result = a.update(b)
print(result)
print(a)
print(b)
None
{'name': 'taro', 'age': 21, 'city': 'Tokyo'}
{'age': 21, 'city': 'Tokyo'}
age は b の 21 に変わり、city が追加されました。ここで押さえておきたいのは update の戻り値が None であることです。次のように書くと、結合結果ではなく None が変数に入ります。
merged = {"a": 1}.update({"b": 2})
print(merged)
None
結合した結果を別の変数で受け取りたいときは、後述の a | b を使うか、merged = a.copy() のあとに merged.update(b) と2行で書きます。
updateが受け取れる引数
update は辞書以外も受け取れます。キーワード引数や、(キー, 値) のペアの並びを渡せるので、1〜2個のキーを足すときにも便利です。
d = {"a": 1}
d.update(b=2, c=3)
print(d)
d = {"a": 1}
d.update([("b", 2)])
print(d)
d = {"a": 1}
d.update(["ab", "cd"])
print(d)
{'a': 1, 'b': 2, 'c': 3}
{'a': 1, 'b': 2}
{'a': 'b', 'c': 'd'}
3つ目は少し意外な結果です。2文字の文字列は「長さ2の並び」として扱われるため、"ab" がキー a・値 b のペアになり、元の a の値 1 が b に上書きされます。
update に文字列のリストを渡すと、意図しないキーができることがあるので、渡す値が辞書かペアの並びかを確かめてから使うと安心です。
縦棒の演算子で新しい辞書を作る(Python 3.9以降)
a | b は、a と b を結合した新しい辞書を返します。 a も b も変わらないので、元のデータを残したいときに向いています。
a = {"name": "taro", "age": 20}
b = {"age": 21, "city": "Tokyo"}
c = a | b
print(c)
print(a)
print(b | a)
{'name': 'taro', 'age': 21, 'city': 'Tokyo'}
{'name': 'taro', 'age': 20}
{'age': 20, 'city': 'Tokyo', 'name': 'taro'}
a | b と b | a では結果が変わります。値は右側が優先、キーの並び順は左側が基準です。b | a では age が a の 20 になり、並びは b の順(age、city)のあとに a だけにある name が続きます。
3.8以前で実行するとどうなるか
縦棒の演算子(|)は Python 3.9 で追加されました。3.8.20 で同じコードを実行すると、次のように止まります。
import sys
print(sys.version.split()[0])
a = {"name": "taro", "age": 20}
b = {"age": 21, "city": "Tokyo"}
print({**a, **b})
try:
print(a | b)
except TypeError as e:
print("TypeError:", e)
3.8.20
{'name': 'taro', 'age': 21, 'city': 'Tokyo'}
TypeError: unsupported operand type(s) for |: 'dict' and 'dict'
3.10.18 では a | b も {**a, **b} も同じ結果になりました。3.8 以前でも動かす必要があるコードでは {**a, **b} を選ぶと、どの版でも同じ結果が得られます。
累積代入の |= は update と同じ働き
a |= b は a 自体を書き換えます。 新しい辞書は作られず、同じオブジェクトのまま内容が更新されます。
a = {"name": "taro", "age": 20}
b = {"age": 21, "city": "Tokyo"}
before = a
a |= b
print(a)
print(before is a)
{'name': 'taro', 'age': 21, 'city': 'Tokyo'}
True
before is a が True なので、a は作り直されず、その場で更新されたことがわかります。関数の引数で受け取った辞書に |= を使うと、呼び出し元の辞書も変わる点は update と同じです。
| と |= で受け取れる型の違い
| は右側も辞書である必要がありますが、|= は update と同じく (キー, 値) のペアの並びも受け取ります。
try:
print({"a": 1} | [("b", 2)])
except TypeError as e:
print("TypeError:", e)
d = {"a": 1}
d |= [("b", 2)]
print(d)
TypeError: unsupported operand type(s) for |: 'dict' and 'list'
{'a': 1, 'b': 2}
アンパックで結合する {**a, **b}(Python 3.5以降)
{**a, **b} は、辞書リテラルの中に a と b を展開して新しい辞書を作る書き方です。結果は a | b と同じで、後ろに書いたものが優先されます。
a = {"name": "taro", "age": 20}
b = {"age": 21, "city": "Tokyo"}
print({**a, **b})
print({**a, **b, "age": 99})
print({**a, "role": "admin"})
{'name': 'taro', 'age': 21, 'city': 'Tokyo'}
{'name': 'taro', 'age': 99, 'city': 'Tokyo'}
{'name': 'taro', 'age': 20, 'role': 'admin'}
結合しながら個別のキーを足したり上書きしたりできるのが、この書き方の強みです。2行目では、b の 21 よりもさらに後ろに書いた 99 が残っています。
dict(a, **b) は文字列キー専用
似た書き方に dict(a, **b) があります。こちらは b をキーワード引数として渡すため、b のキーが文字列でないとエラーになります。
a = {"name": "taro", "age": 20}
print({**a, **{1: "x"}})
try:
print(dict(a, **{1: "x"}))
except TypeError as e:
print("TypeError:", e)
{'name': 'taro', 'age': 20, 1: 'x'}
TypeError: keywords must be strings
キーの型を気にせず使える {**a, **b} か a | b を選んでおくと、後からキーの種類が増えても同じコードで対応できます。
キーが重複したときの結果を並べて確認する
ここまでの方法を、同じ入力で並べて実行します。「どちらの値が残るか」は、渡す順番で決まります。
from collections import ChainMap
a = {"name": "taro", "age": 20}
b = {"age": 21, "city": "Tokyo"}
print("a | b ", a | b)
print("b | a ", b | a)
print("{**a, **b} ", {**a, **b})
print("{**b, **a} ", {**b, **a})
print("ChainMap(a,b)", dict(ChainMap(a, b)))
print("ChainMap(b,a)", dict(ChainMap(b, a)))
c = a.copy()
c.update(b)
print("update ", c)
d = a.copy()
for k, v in b.items():
d.setdefault(k, v)
print("setdefault ", d)
a | b {'name': 'taro', 'age': 21, 'city': 'Tokyo'}
b | a {'age': 20, 'city': 'Tokyo', 'name': 'taro'}
{**a, **b} {'name': 'taro', 'age': 21, 'city': 'Tokyo'}
{**b, **a} {'age': 20, 'city': 'Tokyo', 'name': 'taro'}
ChainMap(a,b) {'age': 20, 'city': 'Tokyo', 'name': 'taro'}
ChainMap(b,a) {'name': 'taro', 'age': 21, 'city': 'Tokyo'}
update {'name': 'taro', 'age': 21, 'city': 'Tokyo'}
setdefault {'name': 'taro', 'age': 20, 'city': 'Tokyo'}
| 方法 | 優先される側 | 覚え方 |
|---|---|---|
a | b、{**a, **b}、a.update(b) |
b(後・右) | 後から書いたものが上書きする |
ChainMap(a, b) |
a(先・左) | 先に見つかったものを返す |
| setdefault のループ | a(既にある値) | キーがないときだけ入れる |
ChainMap だけ優先順が逆(先に渡した側が残る) なので、他の方法から書き換えるときは引数の順番を入れ替えます。
setdefaultで上書きせずに追加する
d.setdefault(key, default) は、キーがなければ default を入れ、既にあれば何もしません。 どちらの場合も、最終的にそのキーに入っている値を返します。
conf = {"host": "localhost"}
print(conf.setdefault("host", "example.com"))
print(conf.setdefault("port", 5432))
print(conf)
localhost
5432
{'host': 'localhost', 'port': 5432}
host は既にあるので localhost のまま、port はなかったので 5432 が入りました。
値がNoneのキーは「ある」と判定される
setdefault が見るのは キーの有無 です。値が None でもキーがあれば上書きされません。
d = {"k": None}
print(d.setdefault("k", 1))
print(d)
None
{'k': None}
「値が None のときも初期値を入れたい」場合は、if d.get("k") is None: d["k"] = 1 のように値を見て判定します。
上書きしない結合の3つの書き方
利用者の設定 user に、足りない項目だけ既定値 defaults から補う例です。既にある theme は残し、ないキーだけ追加します。
defaults = {"theme": "light", "lang": "ja", "page_size": 20}
# 方法1: setdefault のループ(user を書き換える)
user = {"theme": "dark"}
for key, value in defaults.items():
user.setdefault(key, value)
print(user)
# 方法2: 優先したい辞書を右に置く(新しい辞書を作る)
user = {"theme": "dark"}
print(defaults | user)
print({**defaults, **user})
# 方法3: ないキーだけ update に渡す(user を書き換える)
user = {"theme": "dark"}
user.update((k, v) for k, v in defaults.items() if k not in user)
print(user)
{'theme': 'dark', 'lang': 'ja', 'page_size': 20}
{'theme': 'dark', 'lang': 'ja', 'page_size': 20}
{'theme': 'dark', 'lang': 'ja', 'page_size': 20}
{'theme': 'dark', 'lang': 'ja', 'page_size': 20}
4行とも同じ内容になります。defaults | user は右側の user が優先されるので theme は dark のまま、キーの並びは左側の defaults の順(theme、lang、page_size)です。
| 書き方 | user を書き換えるか | 選ぶ目安 |
|---|---|---|
| setdefault のループ | 書き換える | user をそのまま使い続けたい |
defaults | user |
書き換えない | 元の2つを残して結果だけほしい(3.9以降) |
| ないキーだけ update | 書き換える | 条件を足したい(特定のキーだけ補う など) |
ChainMapでコピーせずに重ねて参照する
ChainMap は、複数の辞書を「重ねて1つに見せる」入れ物です。 値をコピーしないので、作成が速く、元の辞書を後から変更すると参照結果にも反映されます。検索は先に渡した辞書から順に行われます。
from collections import ChainMap
defaults = {"theme": "light", "lang": "ja", "page_size": 20}
user = {"theme": "dark"}
settings = ChainMap(user, defaults)
print(settings["theme"], settings["lang"])
print(settings)
print(dict(settings))
# 元の辞書を変えると、参照結果も変わる
defaults["lang"] = "en"
print(settings["lang"])
dark ja
ChainMap({'theme': 'dark'}, {'theme': 'light', 'lang': 'ja', 'page_size': 20})
{'theme': 'dark', 'lang': 'ja', 'page_size': 20}
en
theme は先頭の user から、lang は user にないので defaults から取得されました。dict(settings) と書けば、その時点の内容を普通の辞書として取り出せます。
書き込みと削除は先頭の辞書だけに届く
Python 3.14 の公式ドキュメント(collections の ChainMap の項)には、検索は順にたどり、書き込み・更新・削除は最初のマッピングだけに作用すると書かれています。実行して確かめます。
from collections import ChainMap
defaults = {"theme": "light", "lang": "ja", "page_size": 20}
user = {"theme": "dark"}
settings = ChainMap(user, defaults)
settings["page_size"] = 50
print(user)
print(defaults)
try:
del settings["lang"]
except KeyError as e:
print("KeyError:", e)
print(len(settings), list(settings))
{'theme': 'dark', 'page_size': 50}
{'theme': 'light', 'lang': 'ja', 'page_size': 20}
KeyError: "Key not found in the first mapping: 'lang'"
3 ['theme', 'lang', 'page_size']
page_size への代入は user に入り、defaults は 20 のままです。defaults にしかない lang を削除しようとすると KeyError になります。既定値の辞書を守りながら、上書き分だけを先頭の辞書にためられるのが ChainMap の特長です。
一時的な上書きに便利な new_child と parents
new_child() で先頭に辞書を1枚足し、parents で先頭を外した ChainMap を得られます。
from collections import ChainMap
settings = ChainMap({"theme": "dark"}, {"theme": "light", "lang": "ja"})
temp = settings.new_child({"theme": "blue"})
print(temp["theme"])
print(temp.parents["theme"])
print(settings["theme"])
blue
dark
dark
もとの settings は変わらないので、「この処理の間だけ設定を変える」といった使い方に合います。
ChainMap と dict の使い分け
| 観点 | a | b(辞書を作る) |
ChainMap(b, a)(重ねて見る) |
|---|---|---|
| コピー | する(浅いコピー) | しない |
| 元の辞書の変更 | 結果に反映されない | 反映される |
| 優先される側 | 右側 | 先に渡した側 |
| 書き込み先 | 新しい辞書 | 先頭の辞書 |
| 向いている場面 | 結合結果を保存・受け渡しする | コマンドライン引数、環境変数、既定値のように優先順位を重ねる |
ネストした辞書は浅いマージで丸ごと置き換わる
ここが一番つまずきやすい点です。update、|、|=、{**a, **b}、ChainMap はすべて「浅いマージ」 で、いちばん外側のキーだけを見ます。値が辞書であっても、中身を混ぜ合わせることはしません。
base = {
"db": {"host": "localhost", "port": 5432, "opt": {"ssl": False}},
"debug": False,
}
over = {"db": {"port": 6543}, "debug": True}
print(base | over)
{'db': {'port': 6543}, 'debug': True}
db の中の port だけを変えたかったのに、db 全体が {'port': 6543} に置き換わり、host と opt がなくなりました。 {**base, **over} や base.update(over) でも結果は同じです。
浅いコピーは内側の辞書を共有する
もう1つ知っておきたいのが、a | b で作った新しい辞書は 内側の辞書やリストを元の辞書と共有している ことです。
base = {"db": {"host": "localhost", "port": 5432}}
merged = base | {}
merged["db"]["port"] = 1
print(base["db"]["port"])
print(merged["db"] is base["db"])
1
True
merged 側で内側の値を変えると、base 側も変わります。外側は別の辞書でも、内側は同じオブジェクトを指しているためです。内側まで独立させたいときは、次の再帰マージ関数のように copy.deepcopy を使います。
再帰マージ関数でネストした辞書を結合する
内側の辞書までたどって結合する関数です。標準ライブラリだけで動き、そのままコピーして使えます。元の辞書は変更せず、新しい辞書を返します。
import copy
def deep_merge(base, override):
"""base に override を再帰的に重ねた新しい辞書を返す(元の辞書は変更しない)"""
result = copy.deepcopy(base)
for key, value in override.items():
if isinstance(result.get(key), dict) and isinstance(value, dict):
result[key] = deep_merge(result[key], value)
else:
result[key] = copy.deepcopy(value)
return result
base = {
"db": {"host": "localhost", "port": 5432, "opt": {"ssl": False}},
"debug": False,
}
over = {"db": {"port": 6543}, "debug": True}
merged = deep_merge(base, over)
print(merged)
print(base)
print(merged["db"] is base["db"])
{'db': {'host': 'localhost', 'port': 6543, 'opt': {'ssl': False}}, 'debug': True}
{'db': {'host': 'localhost', 'port': 5432, 'opt': {'ssl': False}}, 'debug': False}
False
db の中は port だけが 6543 に変わり、host と opt は残りました。base は元のまま、内側の辞書も別のオブジェクトになっています。
型が違うとき・リストのときの動き
この関数は 両方が辞書のときだけ再帰 し、それ以外は override 側の値で置き換えます。
print(deep_merge({"a": {"x": 1}}, {"a": 5}))
print(deep_merge({"a": 5}, {"a": {"x": 1}}))
print(deep_merge({"a": [1]}, {"a": [2]}))
{'a': 5}
{'a': {'x': 1}}
{'a': [2]}
| base の値 | override の値 | 結果 |
|---|---|---|
| 辞書 | 辞書 | 中身を再帰的にマージ |
| 辞書 | 辞書以外 | override の値に置き換え |
| 辞書以外 | 辞書 | override の辞書に置き換え |
| リスト | リスト | override のリストに置き換え(連結はしない) |
リストも連結する拡張版
リスト同士は連結したい場合は、分岐を1つ足します。
import copy
def deep_merge_ext(base, override):
"""辞書は再帰マージ、リストは連結、それ以外は override で置き換える"""
result = copy.deepcopy(base)
for key, value in override.items():
current = result.get(key)
if isinstance(current, dict) and isinstance(value, dict):
result[key] = deep_merge_ext(current, value)
elif isinstance(current, list) and isinstance(value, list):
result[key] = current + copy.deepcopy(value)
else:
result[key] = copy.deepcopy(value)
return result
x = {"tags": ["a"], "db": {"hosts": ["h1"]}}
y = {"tags": ["b"], "db": {"hosts": ["h2"], "port": 1}}
print(deep_merge_ext(x, y))
{'tags': ['a', 'b'], 'db': {'hosts': ['h1', 'h2'], 'port': 1}}
「リストは置き換えるのか、連結するのか」は用途で決まるので、関数の説明文(docstring)に方針を書いておくと、あとで読む人が迷いません。
リストの値を足し合わせて結合する
同じキーの値がリストのとき、a | b では後ろのリストで置き換わります。両方の要素を残したいときは、setdefault と extend を組み合わせます。
x = {"fruit": ["apple"], "veg": ["carrot"]}
y = {"fruit": ["banana"], "nut": ["almond"]}
# そのまま結合すると、fruit は y のリストに置き換わる
print(x | y)
# setdefault + extend で足し合わせる
out = {}
for d in (x, y):
for key, values in d.items():
out.setdefault(key, []).extend(values)
print(out)
print(x)
{'fruit': ['banana'], 'veg': ['carrot'], 'nut': ['almond']}
{'fruit': ['apple', 'banana'], 'veg': ['carrot'], 'nut': ['almond']}
{'fruit': ['apple'], 'veg': ['carrot']}
out.setdefault(key, []) は「キーがなければ空のリストを入れ、そのリストを返す」ので、続けて .extend(values) と書けます。元の x は変わっていません。
defaultdict と内包表記でも書ける
from collections import defaultdict
x = {"fruit": ["apple"], "veg": ["carrot"]}
y = {"fruit": ["banana"], "nut": ["almond"]}
merged = defaultdict(list)
for d in (x, y):
for key, values in d.items():
merged[key].extend(values)
print(dict(merged))
print({key: x.get(key, []) + y.get(key, []) for key in x | y})
{'fruit': ['apple', 'banana'], 'veg': ['carrot'], 'nut': ['almond']}
{'fruit': ['apple', 'banana'], 'veg': ['carrot'], 'nut': ['almond']}
| 書き方 | 特徴 |
|---|---|
out.setdefault(k, []).extend(v) |
追加の import がいらない。3つ以上の辞書にもそのまま使える |
defaultdict(list) |
ループの中が短くなる。最後に dict() で普通の辞書に戻せる |
内包表記 x.get(k, []) + y.get(k, []) |
2つの辞書なら1行で書ける |
setdefault に元のリストをそのまま渡すと共有される
out.setdefault(key, values) のように 元のリストそのもの を初期値にすると、out と x が同じリストを指します。
x = {"fruit": ["apple"]}
out = {}
for key, values in x.items():
out.setdefault(key, values)
out["fruit"].append("!!")
print(x["fruit"])
['apple', '!!']
out に追加したつもりが、x も変わりました。初期値は [](新しい空のリスト)にして extend で足す書き方にすると、元のデータと切り離せます。
数値を合計するなら Counter
値が数値で、同じキーを合計したいときは collections.Counter が使えます。
from collections import Counter
s1 = {"apple": 3, "banana": 1}
s2 = {"apple": 2, "cherry": 5}
print(dict(Counter(s1) + Counter(s2)))
print({key: s1.get(key, 0) + s2.get(key, 0) for key in s1 | s2})
print(dict(Counter({"a": 1}) + Counter({"a": -1})))
{'apple': 5, 'banana': 1, 'cherry': 5}
{'apple': 5, 'banana': 1, 'cherry': 5}
{}
3行目のとおり、Counter の足し算は合計が 0 以下になったキーを結果から外します。 0 や負の値も残したいときは、2行目の内包表記を使います。
3つ以上の辞書をまとめて結合する
リストに入った複数の辞書を1つにまとめる書き方です。どの方法でも、後ろにある辞書ほど優先されます(ChainMap は逆順にして渡します)。
from collections import ChainMap
from functools import reduce
dicts = [{"a": 1}, {"b": 2}, {"a": 3, "c": 4}]
# 方法1: update のループ
merged = {}
for d in dicts:
merged.update(d)
print(merged)
# 方法2: reduce と | 演算子
print(reduce(lambda p, q: p | q, dicts, {}))
# 方法3: ChainMap(先頭が優先なので逆順に渡す)
print(dict(ChainMap(*reversed(dicts))))
{'a': 3, 'b': 2, 'c': 4}
{'a': 3, 'b': 2, 'c': 4}
{'a': 3, 'b': 2, 'c': 4}
個数が決まっているなら a | b | c や {**a, **b, **c} と並べるのが簡潔です。個数が決まっていないときは、読みやすい update のループをおすすめします。
つまずきやすいエラーと直し方
実際に出るエラー文と、直し方をまとめます。
tests = [
("{'a': 1} | None", lambda: {"a": 1} | None),
("{'a': 1}.update(None)", lambda: {"a": 1}.update(None)),
("{**{'a': 1}, **None}", lambda: {**{"a": 1}, **None}),
("{'a': 1}.update('abc')", lambda: {"a": 1}.update("abc")),
]
for label, func in tests:
try:
print(label, "->", func())
except Exception as e:
print(label, "->", type(e).__name__ + ":", e)
{'a': 1} | None -> TypeError: unsupported operand type(s) for |: 'dict' and 'NoneType'
{'a': 1}.update(None) -> TypeError: 'NoneType' object is not iterable
{**{'a': 1}, **None} -> TypeError: 'NoneType' object is not a mapping
{'a': 1}.update('abc') -> ValueError: dictionary update sequence element #0 has length 1; 2 is required
| エラー文 | 原因 | 直し方 |
|---|---|---|
unsupported operand type(s) for |: 'dict' and 'dict' |
Python 3.8 以前で | を使った |
{**a, **b} に書き換える、または 3.9 以降で実行する |
unsupported operand type(s) for |: 'dict' and 'NoneType' |
右側が None |
a | (b or {}) のように空の辞書に置き換える |
unsupported operand type(s) for |: 'dict' and 'list' |
右側がペアのリスト | a | dict(pairs) にする |
'NoneType' object is not iterable |
update(None) |
a.update(b or {}) にする |
dictionary update sequence element #0 has length 1; 2 is required |
update に文字列などを渡した | 辞書か (キー, 値) の並びを渡す |
keywords must be strings |
dict(a, **b) で b に文字列以外のキー |
{**a, **b} か a | b にする |
結果が None になる |
c = a.update(b) と書いた |
c = a | b にする |
None が来るかもしれない場面では、次の形が短くて読みやすいです。
a = {"x": 1}
b = None
print(a | (b or {}))
{'x': 1}
速度の目安(1,000キー同士の結合)
キーが 1,000 個の辞書2つ(うち 500 個が重複)を結合する時間を timeit で測りました。数値は環境で変わるので、桁の違いを見る目安として読んでください。
import timeit
from collections import ChainMap
A = {f"k{i}": i for i in range(1000)}
B = {f"k{i}": i for i in range(500, 1500)}
cases = [
("a | b", "A | B"),
("{**a, **b}", "{**A, **B}"),
("copy + update", "d = A.copy(); d.update(B)"),
("dict(ChainMap(b, a))", "dict(ChainMap(B, A))"),
("ChainMap(b, a) の作成のみ", "ChainMap(B, A)"),
]
for label, stmt in cases:
sec = min(timeit.repeat(stmt, globals=globals(), number=2000, repeat=5)) / 2000
print(f"{label:26s} {sec * 1e6:8.2f} µs")
| 書き方 | Python 3.11.15 | Python 3.14.7 |
|---|---|---|
a | b |
25.90 µs | 25.75 µs |
{**a, **b} |
26.90 µs | 26.43 µs |
| copy + update | 26.87 µs | 25.08 µs |
dict(ChainMap(b, a)) |
338.06 µs | 295.49 µs |
ChainMap(b, a) の作成のみ |
0.13 µs | 0.15 µs |
|、アンパック、copy + update の3つはほぼ同じ速さでした。読みやすさで選んで差し支えありません。- ChainMap は 作るだけなら一瞬(コピーしないため)ですが、
dict()で普通の辞書に変換すると、今回の計測では約11〜13倍の時間がかかりました。 - 「重ねて少しだけ参照する」なら ChainMap、「結合した辞書を何度も使う」なら
a | bと使い分けると効率的です。
どの方法を選ぶかの判断表
flowchart TD
A[辞書を結合したい] --> B{元の辞書を書き換えてよい?}
B -- はい --> C{既にあるキーを上書きする?}
C -- はい --> D["a.update(b) / a |= b"]
C -- いいえ --> E["setdefault のループ"]
B -- いいえ --> F{内側の辞書も混ぜたい?}
F -- はい --> G["deep_merge(a, b)"]
F -- いいえ --> H{コピーせずに参照だけしたい?}
H -- はい --> I["ChainMap(b, a)"]
H -- いいえ --> J["a | b(3.8以前は {**a, **b})"]
| 条件 | おすすめ | 理由 |
|---|---|---|
| Python 3.9 以降で、新しい辞書がほしい | a | b |
短く、意図が伝わりやすい |
| 3.8 以前でも動かす | {**a, **b} |
3.5 以降で同じ結果になる |
| 結合しながらキーを足す | {**a, **b, "key": value} |
1つの式で書ける |
| a をその場で更新する | a.update(b) |
どの版でも動き、ペアの並びやキーワード引数も渡せる |
| 既にある値を残す | setdefault のループ、または defaults | user |
上書きを避けられる |
| 設定ファイルのようなネストした辞書 | deep_merge |
内側の辞書を残したまま一部だけ変えられる |
| リストの値を連結する | setdefault(k, []).extend(v) |
置き換えではなく足し合わせになる |
| 優先順位つきの設定を重ねる | ChainMap |
コピーせず、元の変更が反映される |
結合前のチェックリスト
| 確認すること | 見るポイント |
|---|---|
| 元の辞書を変えてよいか | 変えたくなければ a | b か {**a, **b} |
| 重複キーはどちらを残すか | 残したい側を右(後ろ)に置く。ChainMap は先頭 |
| 値に辞書やリストが入っているか | 入っていれば浅いマージで置き換わる。必要なら deep_merge |
| 結合後に内側の値を書き換えるか | 書き換えるなら copy.deepcopy で切り離す |
| 動かす Python の版 | 3.8 以前なら | の代わりに {**a, **b} |
None が渡ってくる可能性 |
b or {} で空の辞書に置き換える |
| update の戻り値を使っていないか | 戻り値は None。結果は a 自体に入る |
よくある質問
Q. update と | 演算子はどちらを使えばよいですか?
A. 元の辞書をその場で更新したいなら a.update(b)、元の辞書を残して新しい辞書がほしいなら a | b です。重複キーの扱い(後から渡した側が残る)と速度は、今回の計測ではほぼ同じでした。Python 3.8 以前でも動かすコードでは、a | b の代わりに {**a, **b} を使います。
Q. 既にあるキーを上書きせずに結合するには?
A. 優先したい辞書を右側に置いて defaults | user と書くか、for k, v in defaults.items(): user.setdefault(k, v) と書きます。前者は新しい辞書を作り、後者は user 自体に足りないキーだけを追加します。
Q. ネストした辞書の一部だけを更新したいのに、全体が置き換わります。
A. update、|、アンパックはいちばん外側のキーだけを見る浅いマージのため、値が辞書でも丸ごと置き換えます。本文の deep_merge のように、両方の値が辞書のときだけ再帰する関数を使うと、内側のキーを残したまま一部だけ更新できます。
Q. ChainMap と辞書の結合は何が違いますか?
A. ChainMap は値をコピーせず、複数の辞書を順番に検索する入れ物です。元の辞書を変更すると参照結果も変わり、書き込みは先頭の辞書だけに届きます。優先されるのが「先に渡した辞書」である点も、右側優先の a | b と逆になります。
まとめ
- 元の辞書を書き換えるのは update と |=、新しい辞書を作るのは | と {**a, **b} です。update の戻り値は
Noneです。 - キーが重複したときは、後から渡した側(右側)の値が残ります。 ChainMap と setdefault は、先にあった値が残ります。
- | と |= は Python 3.9 以降で使えます。3.8.20 では TypeError になることを確認しました。3.8 以前では
{**a, **b}を使います。 - 標準の結合方法はすべて浅いマージで、ネストした辞書は丸ごと置き換わります。内側まで結合するには再帰マージ関数を使います。
- 上書きしたくないときは setdefault、または優先したい辞書を右に置く
defaults | userが使えます。 - リストの値は
setdefault(k, []).extend(v)で足し合わせ、数値の合計には Counter が使えます。 - ChainMap はコピーせずに重ねて参照でき、作成は速い一方、辞書への変換には時間がかかります。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Python辞書の結合・マージ updateと|演算子とsetdefault」
株式会社RJCでは、Pythonの辞書の結合やデータ構造の扱い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。