collections

【Python】ChainMapで複数の辞書を連結する(collections.ChainMap)

【Python】ChainMapで複数の辞書を連結する(collections.ChainMap)

複数の辞書を連結する際に利用できる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] = value

for文では、既に設定されている辞書のリスト「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の方が処理が早いという記載があります。

是非、辞書の連結の際には使用を検討してもらえればと思います。

Note

ChainMapの公式ドキュメントの記載はこちらを参照してください。