既定値、装置ごとの条件、今回だけの指定を重ねたいなら、collections.ChainMapで辞書を優先順に並べられる。検索は先頭の辞書から行われ、最初に見つかった値が使われる。結合したコピーを作る方法と違い、元の辞書の変更も見える。ただし、代入や削除は基本的に先頭の辞書へ作用するので、その範囲を理解して使おう。
優先順位の高い辞書を先頭に置く
解析条件を、共通のdefaults、装置固有のdevice、今回のoverridesに分ける。今回だけ指定した項目を最優先、次に装置設定、最後に共通設定としたいなら、ChainMap(overrides, device, defaults)の順に渡す。辞書の結合演算で右側を優先する場合とは、書く順序が違う。
ChainMapは元の辞書をコピーして一つへ詰め直すのではなく、参照する辞書の並びを持つ。そのため、あとから既定値を変更すれば、上位の辞書で覆われていない項目には変更が反映される。設定の現在値を重ねて見たい場合に向いている。
取得・代入・元データの変更を確認する
example.py
from collections import ChainMap
defaults = {"gain": 1, "timeout": 10, "mode": "normal"}
device = {"gain": 2}
overrides = {"gain": 3}
config = ChainMap(overrides, device, defaults)
print("effective:", dict(sorted(config.items())))
config["timeout"] = 5
print("write target:", overrides)
assert defaults["timeout"] == 10 and overrides["timeout"] == 5
snapshot = dict(config)
defaults["mode"] = "fast"
print("live/snapshot:", config["mode"], snapshot["mode"])
assert config["mode"] == "fast" and snapshot["mode"] == "normal"
child = config.new_child({"gain": 8})
print("child/parent:", child["gain"], config["gain"])
assert child["gain"] == 8 and config["gain"] == 3
try:
del config["mode"]
except KeyError:
print("delete lower layer: KeyError")
else:
raise AssertionError("Deletion should only target the first mapping")
実行結果
effective: {'gain': 3, 'mode': 'normal', 'timeout': 10}
write target: {'gain': 3, 'timeout': 5}
live/snapshot: fast normal
child/parent: 8 3
delete lower layer: KeyError
gainは今回の3、timeoutは共通の10が使われる。config[“timeout”]へ5を代入すると、defaultsの値を探して書き換えるのではなく、先頭のoverridesへ追加される。defaultsのtimeoutは10のままで、以後は今回指定の5が優先される。
代入先と値を見つけた場所は違う
読み取り時には複数の辞書を順に探すが、書き込み時には先頭だけを変更する。この違いを知らないと、既定値を修正したつもりが今回の上書きを作っただけ、ということが起こる。元の設定を変更したいなら、意図する辞書へ直接書き込む方が明確である。
削除も先頭の辞書を対象にする。下位の辞書にだけあるキーをdel config[key]で消そうとすると、表示上は値が見えていてもKeyErrorになる。先頭の上書きを削除した場合には、下位の値が再び見えることもあるため、キーを全体から消した意味にはならない。
一時的な条件にはnew_childを使う
new_childへ辞書を渡すと、その辞書を最優先にした新しいChainMapを作れる。例ではgainだけを8にしている。元のconfigのgainは3のままであり、入れ子の処理や短い試行の条件を重ねるときに使いやすい。
new_childも下位の辞書を複製するわけではない。元のdefaultsやdeviceが変われば、上書きされていない値は子側にも影響する。完全に独立した実験条件を固定したい場合には、別途スナップショットを作る必要がある。
確定した値が必要ならdictへ変換する
dict(config)は、その時点で各キーを解決した通常の辞書を作る。例ではsnapshotを作った後でdefaultsのmodeを変えても、snapshotの最上位のmodeは以前の文字列のままになる。一方、ChainMapからは変更後の値が見える。
ただし、値がリストや辞書ならsnapshot側とも共有される。dictへ変換しただけで深く独立するわけではない。実行条件を後日再現したい用途では、必要な型へ整えて保存するなど、値そのものの固定方法も考える必要がある。
優先順位と表示順を混同しない
ChainMapを反復したときのキー順は、単純に先頭辞書の項目から読み上げる順序ではない。値を検索する優先順位と、キーを列挙する順序を分けて理解する。レポートで順序が重要なら、例のようにsortedで明示する方が読み比べやすい。
値がFalse、0、Noneであっても、そのキーが先頭の辞書に存在すれば上位の指定として使われる。「空っぽに見える値なら下位へ落ちる」という動作ではない。Noneを未指定の意味にしたい場合は、設定を重ねる前にキーを除くなど、別の規則を設ける。
元の辞書へ追従する設定が欲しいか、実行時点で固定された設定が欲しいかを最初に決めよう。前者にはChainMap、後者にはコピーや記録が向いている。検索の優先順位だけでなく、変更をどこへ反映したいかまで含めて選ぶのが大切である。
動作確認と参考資料
掲載例はLinux・CPython 3.12.14で動作確認した。OS固有のコマンドや環境ごとに変わるパスは、本文中の条件を確認して使ってほしい。
