静かなる名辞

pythonとプログラミングのこと


【python】collections.ChainMapの使い方を理解する

はじめに

 pythonで複数の辞書をマージするにはどうしたらいいのでしょうか。forループ? 辞書内包表記を使う? updateメソッド? 実は、ChainMapというものもあります。

 その使い方について説明します。

先に結論

 記事本文でだらだらと説明していますが、要約すると、

  • 複数の辞書を一つにまとめる方法
  • 元辞書への参照を張ってごにょごにょするだけなので、新しい辞書を作るより(ケースバイケースだが)速い。ただしマージが速くても参照が遅いという微妙な性質がある
  • 当然元辞書を変更すると反映されてしまう

 これだけです。

基本的な使い方

 リストを渡すのかと思ったら、違うようです。

>>> from collections import ChainMap
>>> d1 = {"hoge":"ほげ"}
>>> d2 = {"fuga":"ふが"}
>>> cm = ChainMap(d1, d2)
>>> cm["hoge"]
'ほげ'
>>> cm["fuga"]
'ふが'

 このように使えます。ただ、辞書のリストを動的に生成して渡したい場合もあるでしょう。その場合、starred expressionを活用して、

>>> cm = ChainMap(*[d1, d2])
>>> cm
ChainMap({'hoge': 'ほげ'}, {'fuga': 'ふが'})

 このようにやることになるかと思います。

参照先を変更してみる

>>> d1["HOGE"] = "ホゲ"
>>> cm
ChainMap({'hoge': 'ほげ', 'HOGE': 'ホゲ'}, {'fuga': 'ふが'})

 このようにけっこう恐ろしい性質があります。これが嫌なときは他の方法で辞書をマージするか、deepcopyを使うことになるはずです。

 参考:複数の辞書のマージ方法いろいろ - Qiita

速いらしいという噂があるので測る

 測ってみました。上記参考サイトの辞書内包表記版と比較します。

 予想では、マージは速いものの探索は恐らくhashのリストを線形探索していく上、重複を取り除く処理などもあるので、遅い要素がありそうです。

 items()と一つのキーへのアクセスで近似的に探索を表現します。

# coding: UTF-8

import time
from collections import ChainMap

def time_measure(f, lst):
    time_lst = []
    for i in range(10000):
        t1 = time.time()
        result = f(lst)
        t2 = time.time()
        time_lst.append(t2-t1)
    return result, sum(time_lst)/10000

def main():
    f1 = lambda dicts:{k: v for dic in dicts for k, v in dic.items()}
    f2 = lambda dicts:ChainMap(*dicts)

    d1 = dict(zip(range(100), range(100)))
    d2 = dict(zip("ho", "ge"))
    d3 = dict(zip(range(100), "p"*100))

    result, time = time_measure(f1, [d1, d2, d3])
    print("辞書内包マージ", time)
    f = lambda _:[(k,v) for k,v in result.items()]
    _, time = time_measure(f, None)
    print("辞書内包items()", time)
    f = lambda _:result["h"]
    _, time = time_measure(f, None)
    print("辞書内包キーアクセス", time)
    result, time = time_measure(f2, [d1, d2, d3])
    print("ChainMapマージ", time)
    f = lambda _:[(k,v) for k,v in result.items()]
    _, time = time_measure(f, None)
    print("ChainMap items()", time)
    f = lambda _:result["h"]
    _, time = time_measure(f, None)
    print("ChainMapキーアクセス", time)

if __name__ == "__main__":
    main()

 結果は

辞書内包マージ 1.2324166297912597e-05
辞書内包items() 7.125544548034668e-06
辞書内包キーアクセス 1.9309520721435546e-07
ChainMapマージ 1.3034582138061524e-06
ChainMap items() 6.01630687713623e-05
ChainMapキーアクセス 1.183009147644043e-06

 マージは若干遅いが探索は速い辞書、マージは速いものの肝心の探索が遅いChainMapという結論。

 探索速度(実タスクでの探索速度ではないが)に1桁の差が出てる訳で、安直に「ChainMap使おう」とはならないと思う・・・。普通はアクセスの速さを享受したいから辞書にすると思うのだが。

まとめ

 使い所は難しいと思いました。