collections

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

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

複数の辞書を連結する際に利用できる collections モジュールの ChainMap について解説します。

ChainMapcollections モジュール)

Python で複数の辞書を連結して使用したい場合には、collections モジュールの ChainMap を使用することができます。

複数の辞書を update を使うことでも同様のことができますが、ChainMap はもととなった辞書がどういったものであったかをチェーンとして記録しつつ辞書の連結をすることができます。update とは同じキーの値が存在するとき、上書きされてしまい過去どういったデータであったかを確認することはできない点で異なります。

公式ドキュメントのこちらでは、辞書の update を繰り返すよりはたいていの場合、ChainMap の方が処理が早いという記載もあるため、辞書連結の際には ChainMap の使用を検討する価値があります。

本記事では、collections モジュールの ChainMap について基本的な使い方を紹介します。

dictupdate で辞書を連結する場合

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_bdict_c の順に辞書を update しています。

dict_bupdate した際に、キー "b" の値は "BB" に上書きされます。次に dict_cupdate した際には、キー "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_cdict_bdict_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 で該当するキーを削除しています。

一通りすべてを探索した結果、該当するキーを含む辞書がない場合には、エラーとして KeyErrorraise でスローしています。

以上のようにすることで、チェーン全体に対して更新・削除をすることができます。

まとめ

複数の辞書を連結する際に利用できる collections モジュールの ChainMap について解説しました。

複数の辞書を update を使うことでも同様のことができますが、ChainMap はもとの辞書がどういったものであったかをチェーンとして記録しつつ辞書の連結をすることができます。公式ドキュメントでも、辞書の update を繰り返すよりはたいていの場合、ChainMap の方が処理が早いという記載があります。

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

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

ソースコード

上記で紹介しているソースコードについては GitHub にて公開しています。参考にしていただければと思います。

あわせて読みたい
【Python Tech】プログラミングガイド
【Python Tech】プログラミングガイド
ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。 私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は大手電機メーカーでシステム開発に携わっていました。

プログラミング言語はこれまでC、C++、JAVA等を扱ってきましたが、最近では特に機械学習等の分析でも注目されているPythonについてとても興味をもって取り組んでいます。これまでの経験をもとに、Pythonに興味を持つ方のお役に立てるような情報を発信していきたいと思います。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました