【Python】ChainMapで複数の辞書を連結する(collections.ChainMap)
.jpg)
複数の辞書を連結する際に利用できる collections モジュールの ChainMap について解説します。
目次
ChainMap(collections モジュール)
Python で複数の辞書を連結して使用したい場合には、collections モジュールの ChainMap を使用することができます。
複数の辞書を update を使うことでも同様のことができますが、ChainMap はもととなった辞書がどういったものであったかをチェーンとして記録しつつ辞書の連結をすることができます。update とは同じキーの値が存在するとき、上書きされてしまい過去どういったデータであったかを確認することはできない点で異なります。
公式ドキュメントのこちらでは、辞書の update を繰り返すよりはたいていの場合、ChainMap の方が処理が早いという記載もあるため、辞書連結の際には ChainMap の使用を検討する価値があります。
本記事では、collections モジュールの ChainMap について基本的な使い方を紹介します。
dict の update で辞書を連結する場合
ChainMap との比較のため、まずは辞書を複数用意して update する例について見てみます。ChainMap の使用方法にのみ興味がある方は読み飛ばしてください。
dict_a = {"a": "A", "b": "B"}
dict_b = {"b": "BB", "c": "CC"}
dict_c = {"b": "BBB", "c": "CCC"}
dict_a.update(dict_b)
print(dict_a)
dict_a.update(dict_c)
print(dict_a)【実行結果】
{'a': 'A', 'b': 'BB', 'c': 'CC'}
{'a': 'A', 'b': 'BBB', 'c': 'CCC'}上記例では、dict_a に対して dict_b、dict_c の順に辞書を update しています。
dict_b で update した際に、キー "b" の値は "BB" に上書きされます。次に dict_c で update した際には、キー "b" の値は "BBB" に、キー "c" の値は "CCC" に上書きされます。
この方法で辞書を更新する場合は、それぞれのキーに対して更新した過去の値がもともとどういった値であったのかは、後から確認することができません。
ChainMap で辞書を連結する場合
ChainMap を用いて辞書の結合する場合は、以下の例のように使用します。
import collections
dict_a = {"a": "A", "b": "B"}
dict_b = {"b": "BB", "c": "CC"}
dict_c = {"b": "BBB", "c": "CCC"}
# ChainMapを用いて辞書を連結する
d_map = collections.ChainMap(dict_c, dict_b, dict_a)
print(d_map)
print(type(d_map.maps), d_map.maps)
print(
f"d_map['a'] = {d_map['a']}, d_map['b'] = {d_map['b']}, "
f"d_map['c'] = {d_map['c']}"
)
print("=====")
# 連結順を逆転させる
d_map.maps.reverse()
print(d_map)
print(type(d_map.maps), d_map.maps)
print(
f"d_map['a'] = {d_map['a']}, d_map['b'] = {d_map['b']}, "
f"d_map['c'] = {d_map['c']}"
)【実行結果】
ChainMap({'b': 'BBB', 'c': 'CCC'}, {'b': 'BB', 'c': 'CC'}, {'a': 'A', 'b': 'B'})
<class 'list'> [{'b': 'BBB', 'c': 'CCC'}, {'b': 'BB', 'c': 'CC'}, {'a': 'A', 'b': 'B'}]
d_map['a'] = A, d_map['b'] = BBB, d_map['c'] = CCC
=====
ChainMap({'a': 'A', 'b': 'B'}, {'b': 'BB', 'c': 'CC'}, {'b': 'BBB', 'c': 'CCC'})
<class 'list'> [{'a': 'A', 'b': 'B'}, {'b': 'BB', 'c': 'CC'}, {'b': 'BBB', 'c': 'CCC'}]
d_map['a'] = A, d_map['b'] = B, d_map['c'] = CC以降でポイントについて順に見ていきます。
# ChainMapを用いて辞書を連結する d_map = collections.ChainMap(dict_c, dict_b, dict_a)
ChainMap を使用して辞書を連結する場合には、上記のように辞書を順に引数に渡して ChainMap をインスタンス化します。この ChainMap のインスタンスは、通常の辞書と同様に、[] でキーを指定してアクセスが可能です。
print(type(d_map.maps), d_map.maps)
ChainMapでは、「.maps」とすると、連結に使用された辞書をリストで取得することができます。これにより、連結に使用した辞書を後から参照して利用可能です。
引数で dict_c、dict_b、dict_a という順番で指定した場合、一番右の辞書に対して順番に左方向に辞書の update を実施した結果と同じになります。
もし、引数に指定した辞書と順序を逆にしたい場合には、以下のように reverse メソッドを実行することで順序を逆転させることもできます。
d_map.maps.reverse()
以上が、ChainMap の基本的な使い方になります。
ChainMap のチェーン上の辞書を更新、削除する
ChainMap で連結した辞書を参照する際には、連結したチェーン全体に対して探索を行って表示することができます。しかし、更新や削除については引数の最初に指定する辞書に対してのみに行います。
ChainMap での更新、削除する
ChainMap に対して更新や削除をする場合、最初に位置する辞書に対してのみが対象となることを例で見てみます。
import collections
dict_a = {"a": "A"}
dict_b = {"b": "B"}
dict_c = {"c": "C"}
# ChainMapを用いて辞書を連結する
d_map = collections.ChainMap(dict_c, dict_b, dict_a)
print(d_map)
print("=====")
# 値を更新する
d_map["b"] = "B_update"
print(d_map)
print("=====")
# 値を削除する
del d_map["a"]
print(d_map)【実行結果】
ChainMap({'c': 'C'}, {'b': 'B'}, {'a': 'A'})
=====
ChainMap({'c': 'C', 'b': 'B_update'}, {'b': 'B'}, {'a': 'A'})
=====
Traceback (most recent call last):
...(省略)...
KeyError: "Key not found in the first mapping: 'a'"この例では、キーが "b" の値を更新しようとしていますが、一番最初の引数として指定した辞書に "b" がキーとなる値が追加されており、2番目の辞書の "b" の値が更新されるわけではないことが分かります。
また、キー "a" について削除をしようとしていますが、こちらについては「KeyError」ということで例外が発生しています。これは、一番最初の引数として指定した辞書に "a" がキーとなる値が含まれないことが理由です。なお、キー "b" や "c" を削除する場合は問題ありません。
この例から、更新や削除が一番最初の引数として指定した辞書に対してのみ行われることが分かります。
ChainMap のチェーンを探索して更新・削除する
チェーンの深いところまでたどって更新や削除をしたい場合は、ChainMap を継承して以下のような DeepChainMap というクラスを作成することで、深いチェーンで見つかったキーに対して更新、削除ができます。
なお、このプログラムは、公式ドキュメントのこちらで紹介されています。
import collections
class DeepChainMap(collections.ChainMap):
def __setitem__(self, key, value):
for mapping in self.maps:
if key in mapping:
mapping[key] = value
return
self.maps[0][key] = value
def __delitem__(self, key):
for mapping in self.maps:
if key in mapping:
del mapping[key]
return
raise KeyError(key)
def main():
dict_a = {"a": "A"}
dict_b = {"b": "B"}
dict_c = {"c": "C"}
deep_m = DeepChainMap(dict_c, dict_b, dict_a)
print(deep_m)
print("=====")
# キーが"b"であるものを探索して更新する
deep_m["b"] = "b_update"
print(deep_m)
print("=====")
# キーが"a"であるものを探索して削除する
del deep_m["a"]
print(deep_m)
if __name__ == "__main__":
main()【実行結果】
DeepChainMap({'c': 'C'}, {'b': 'B'}, {'a': 'A'})
=====
DeepChainMap({'c': 'C'}, {'b': 'b_update'}, {'a': 'A'})
=====
DeepChainMap({'c': 'C'}, {'b': 'b_update'}, {})この例では、ChainMap を継承して値を設定する「__setitem__」メソッドとキー・値を削除する「__delitem__」メソッドをオーバーライドしています。
まずは、__setitem__ の内容から見てみます。
def __setitem__(self, key, value):
for mapping in self.maps:
if key in mapping:
mapping[key] = value
return
self.maps[0][key] = valuefor 文では、既に設定されている辞書のリスト「self.maps」から順に辞書を取り出して mapping に設定し、その辞書に指定した key が含まれている場合は、key の値を更新して、return でメソッドを終了します。
一通りすべてを探索した結果、該当するキーを含む辞書がなかった場合には、入力されたキー(key)・値(value)を一番最初の辞書「self.maps[0]」に新しく追加します。
同様に削除の方の __delitem__ の内容も見てみましょう。
def __delitem__(self, key):
for mapping in self.maps:
if key in mapping:
del mapping[key]
return
raise KeyError(key)この例も、基本的な構造は __setitem__ と同じで、key に該当するものを探索して、一致したら del で該当するキーを削除しています。
一通りすべてを探索した結果、該当するキーを含む辞書がない場合には、エラーとして KeyError をraise でスローしています。
以上のようにすることで、チェーン全体に対して更新・削除をすることができます。
まとめ
複数の辞書を連結する際に利用できる collections モジュールの ChainMap について解説しました。
複数の辞書を update を使うことでも同様のことができますが、ChainMap はもとの辞書がどういったものであったかをチェーンとして記録しつつ辞書の連結をすることができます。公式ドキュメントでも、辞書の update を繰り返すよりはたいていの場合、ChainMap の方が処理が早いという記載があります。
是非、辞書の連結の際には使用を検討してもらえればと思います。
上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。


.jpg)
.jpg)
.jpg)
.jpg)