[解決!Python]ChainMapクラスを使って複数のマッピング(辞書)をまとめて扱うには:解決!Python
Collectionsモジュールが提供するChainMapクラスを使って複数の辞書をひとまとめにして取り扱うための基本的な方法とその注意点、簡単な例を示す。
from collections import ChainMap
d0 = {'a': 1, 'b': 2}
d1 = {'b': 3, 'c': 4}
d2 = {'c': 5, 'd': 6}
cm0 = ChainMap(d2, d1, d0) # リストの後ろの辞書が、前にある辞書で上書きされる
print(cm0) # ChainMap({'c': 5, 'd': 6}, {'b': 3, 'c': 4}, {'a': 1, 'b': 2})
print(cm0['a']) # 1
print(cm0['b']) # 3
print(cm0['c']) # 5
# ChainMapのキーと値を反復
for k, v in cm0.items():
print(f'{k}: {v}')
# 出力結果:
#a: 1
#b: 3
#c: 5
#d: 6
# 以下と同様
d = d0.copy() # {'a': 1, 'b': 2}
d.update(d1) # {'b': 3, 'c': 4}で更新
d.update(d2) # {'c': 5, 'd': 6}で更新
for k, v in d.items():
print(f'{k}: {v}')
# ChainMapを構成するマッピングを一覧する
print(cm0.maps) # [{'c': 5, 'd': 6}, {'b': 3, 'c': 4}, {'a': 1, 'b': 2}]
cm0.maps[0]['c'] = 50 # ChainMapのマッピングはリストでアクセスできる
print(cm0) # ChainMap({'c': 50, 'd': 6}, {'b': 3, 'c': 4}, {'a': 1, 'b': 2})
print(d2) # {'c': 50, 'd': 6}:元のマッピングでも変更が反映される
# ChainMapの更新は先頭のマッピングに対して行われる
cm1 = ChainMap(d1, d0)
print(cm1) # ChainMap({'b': 3, 'c': 4}, {'a': 1, 'b': 2})
cm1['b'] = 30
print(cm1) # ChainMap({'b': 30, 'c': 4}, {'a': 1, 'b': 2})
print(d1) # {'b': 30, 'c': 4}:元のマッピングでも変更が反映される
cm1['foo'] = 'FOO'
print(cm1) # ChainMap({'b': 30, 'c': 4, 'foo': 'FOO'}, {'a': 1, 'b': 2})
print(d1) # {'b': 30, 'c': 4, 'foo': 'FOO'}
del(cm1['a']) # KeyError
# 元のマッピングに影響を及ぼしたくない場合はnew_child()メソッドを使う
d0 = {'a': 1, 'b': 2}
d1 = {'b': 3, 'c': 4}
cm1 = ChainMap(d1, d0)
cm2 = cm1.new_child() # 現在のChainMapから独立したChainMapを作成
print(cm2) # ChainMap({}, {'b': 30, 'c': 4}, {'a': 1, 'b': 2})
cm2['b'] = 30
print(cm2) # ChainMap({'b': 30}, {'b': 3, 'c': 4}, {'a': 1, 'b': 2})
cm2.maps[1]['b'] = 33 # 元のマッピングに影響を及ぼすことは可能
print(cm2) # ChainMap({'b': 30}, {'b': 33, 'c': 4}, {'a': 1, 'b': 2})
print(d1) # {'b': 33, 'c': 4}
print(cm2['b']) # 30
d0 = {'a': 1, 'b': 2}
d1 = {'b': 3, 'c': 4}
d2 = {'c': 5, 'd': 6}
cm3 = ChainMap(d2, d1, d0)
print(cm3) # ChainMap({'c': 5, 'd': 6}, {'b': 3, 'c': 4}, {'a': 1, 'b': 2})
print(cm3.parents) # ChainMap({'b': 3, 'c': 4}, {'a': 1, 'b': 2})
ChainMapクラス
Pythonに標準で付属するcollectionsモジュールでは、複数のマッピング(辞書)を一つの辞書のように扱うためのChainMapクラスが定義されている。複数の階層(例:グローバル設定とユーザー設定)で構成される設定など、特定のキーに対する値がネストする状況を扱う際にChainMapクラスを使うとよい。
ChainMapオブジェクトの作成
簡単な例を以下に示す。
from collections import ChainMap
d0 = {'a': 1, 'b': 2}
d1 = {'b': 3, 'c': 4}
d2 = {'c': 5, 'd': 6}
cm0 = ChainMap(d2, d1, d0) # リストの後ろにある辞書が、前にある辞書で上書きされる
この例では、3つの辞書からChainMapクラスのオブジェクトを作成している。実際のChainMapオブジェクトと、キーに対応する値を表示してみると次のようになる。
print(cm0) # ChainMap({'c': 5, 'd': 6}, {'b': 3, 'c': 4}, {'a': 1, 'b': 2})
print(cm0['a']) # 1
print(cm0['b']) # 3
print(cm0['c']) # 5
1行目の出力結果を見ると、ChainMapオブジェクトは元の辞書を引数リストに指定した順番で内部に持っていることが分かる(元の辞書のコピーではなく、参照として持っている)。そして、ChainMapオブジェクトに対してキーを指定すると内部に持つ辞書のリストの先頭から末尾へと向かって、そのキーに対応する値を検索して、対応する値があれば、それがそのキーの値となる。キー'b'はd0にもd1にもあるが、d1の方が引数リストで先頭に近いところに指定されたので、ここではd1['b']の値である3が得られている。キー'c'についても同様だ。
言い方を変えると、引数リストで末尾にあるものがChainMapオブジェクトのキー/値の組のベースとなり、引数リストでそれよりも前方にあるものでベースを更新していく(上書きしていく)とイメージできる。
要素の反復
ChainMapオブジェクトは通常のマッピングと同様に反復が可能だ。ここでは例として、itemsメソッドを使って、キーと値の両者を反復してみよう。
for k, v in cm0.items():
print(f'{k}: {v}')
# 出力結果:
#a: 1
#b: 3
#c: 5
#d: 6
これは以下のコードの実行結果と同様である。上でも述べたように、末尾のマッピングがそれよりも前にあるマッピングで順次更新されていることが分かるはずだ。
d = d0.copy() # {'a': 1, 'b': 2}
d.update(d1) # {'b': 3, 'c': 4}で更新
d.update(d2) # {'c': 5, 'd': 6}で更新
for k, v in d.items(): # 出力結果は省略
print(f'{k}: {v}')
ChainMapオブジェクトを構成するマッピングの取得
ChainMapオブジェクトを構成するマッピングはmaps属性で取得できる。
print(cm0.maps) # [{'c': 5, 'd': 6}, {'b': 3, 'c': 4}, {'a': 1, 'b': 2}]
maps属性を使って、元のマッピングを直接操作することも可能だ。
cm0.maps[0]['c'] = 50 # ChainMapのマッピングはリストでアクセスできる
print(cm0) # ChainMap({'c': 50, 'd': 6}, {'b': 3, 'c': 4}, {'a': 1, 'b': 2})
print(d2) # {'c': 50, 'd': 6}:元のマッピングでも変更が反映される
この例では、cm0.maps[0]として元のマッピング(d2)のキー'c'の値を変更している。これによりd2とcm0の値がどちらも変更される。
ChainMapオブジェクトの変更
ChainMapオブジェクトを使って、要素を削除したり変更したりする際には、その対象は先頭のマッピングとなる。
以下はd1とd0で構成されるChainMapオブジェクトを新規に作成して、そのキー'b'の値を変更するコード例だ。
cm1 = ChainMap(d1, d0)
print(cm1) # ChainMap({'b': 3, 'c': 4}, {'a': 1, 'b': 2})
cm1['b'] = 30
print(cm1) # ChainMap({'b': 30, 'c': 4}, {'a': 1, 'b': 2})
print(d1) # {'b': 30, 'c': 4}:元のマッピングでも変更が反映される
出力結果を見ると、変更されるのは先頭のマッピングであることと、元の辞書(d1)にも変更が及んでいることが分かる。
要素の追加も同様だ。
cm1['foo'] = 'FOO'
print(cm1) # ChainMap({'b': 30, 'c': 4, 'foo': 'FOO'}, {'a': 1, 'b': 2})
print(d1) # {'b': 30, 'c': 4, 'foo': 'FOO'}
キー'foo'と対応する値を与えると、先頭のマッピングが変更されていることが分かる。
削除も同様に先頭のマッピングに対して行われる。cm1の先頭のマッピングにはキー'a'がないので、以下のコードはKeyError例外を発生する。
del(cm1['a']) # KeyError
こうした挙動にはちょっと注意が必要かもしれない。
new_childメソッドで元のマッピングに影響が出ないようにする
ChainMapオブジェクトで要素を追加したり変更したりした影響が元のマッピングに影響を及ぼすのがイヤなときには、先頭のマッピングを別途用意するとよい。これにはChainMapのnew_childメソッドを試用する。
d0 = {'a': 1, 'b': 2}
d1 = {'b': 3, 'c': 4}
cm1 = ChainMap(d1, d0)
cm2 = cm1.new_child() # 現在のChainMapから独立したChainMapを作成
これはd1とd0からChainMapオブジェクトを作成し、new_childメソッドでその新しい子(ChainMapオブジェクト)を作成している。どんなオブジェクトが作成されるかというと次のようなものだ。
print(cm2) # ChainMap({}, {'b': 30, 'c': 4}, {'a': 1, 'b': 2})
更新や変更の対象となる先頭のマッピングが空のChainMapオブジェクトが作成されているだけである。実際にはnew_childメソッドには先頭のマッピングの初期値として辞書やキーワード引数を指定することも可能だ。
このChainMapオブジェクトでキー'b'の値を30に変更してみよう。
cm2['b'] = 30
print(cm2) # ChainMap({'b': 30}, {'b': 3, 'c': 4}, {'a': 1, 'b': 2})
すると、変更対象のマッピングにキーと値が追加される。
なお、maps属性を使えば、元のマッピングに影響を及ぼすことは依然として可能だ。
cm2.maps[1]['b'] = 33 # 元のマッピングに影響を及ぼすことは可能
print(cm2) # ChainMap({'b': 30}, {'b': 33, 'c': 4}, {'a': 1, 'b': 2})
print(d1) # {'b': 33, 'c': 4}
この例ではmaps属性を使って、元のマッピングを直接変更している。その結果、元のマッピングのキー'b'の値が33になっている。しかし、先頭のマッピングにもキー'b'があるので、次のようにChainMapオブジェクトにキー'b'を指定しても、この変更は見えない。
print(cm2['b']) # 30
こうしたことから、new_childメソッドで新しくChainMapオブジェクトを作成するときには、そのオブジェクトのmaps属性を使って元のマッピングを直接変更するようなことはしない方がよいだろう(そのためにnew_childメソッドを使うのだから)。
parents属性で先頭のマッピング以外のマッピングを取得する
ChainMapオブジェクトにはparents属性もある。これは先頭のマッピング以外のマッピングを取得するためのものだ。
d0 = {'a': 1, 'b': 2}
d1 = {'b': 3, 'c': 4}
d2 = {'c': 5, 'd': 6}
cm3 = ChainMap(d2, d1, d0)
print(cm3) # ChainMap({'c': 5, 'd': 6}, {'b': 3, 'c': 4}, {'a': 1, 'b': 2})
print(cm3.parents) # ChainMap({'b': 3, 'c': 4}, {'a': 1, 'b': 2})
この属性は例えば、ChainMapでスコープの処理をエミュレートするような場合に役に立つかもしれない。お行儀が悪いが以下に例を示す。
x = 10
def test():
x = 20
scopes = ChainMap(locals(), globals())
scopes.parents['x'] = 30
print(x)
print(x)
この例では、グローバル変数xに10を代入している。また、test関数の中ではローカル変数xに20を代入した後、locals関数とglobals関数の戻り値からChainMapオブジェクトを作成している。そのparents属性はつまりglobals関数の戻り値であり、その'x'属性の値を30に変更している。これにより、ローカル変数xではなく、グローバル変数xの値が変更される。
ちょっとした例
ChainMapクラスの使用例として、簡単なコードを以下に示す。まずは2つのjsonファイルを用意する。base.jsonファイルはベースライン設定を、user.jsonファイルはユーザー設定を表すものとしよう。
from pathlib import Path
print(Path('base.json').read_text())
# 出力結果:
#{
# "width": 80,
# "height": 25,
# "forecolor": "white",
# "backcolor": "black"
#}
print(Path('user.json').read_text())
# 出力結果:
#{
# "forecolor": "black",
# "backcolor": "white",
# "fontsize": 12
#}
これらの内容を読み込んで辞書に格納する。
import json
base = json.loads(Path('base.json').read_text())
user = json.loads(Path('user.json').read_text())
print(base)
# 出力結果:
#{'width': 80, 'height': 25, 'forecolor': 'white', 'backcolor': 'black'}
print(user)
# 出力結果:
#{'forecolor': 'black', 'backcolor': 'white', 'fontsize': 12}
そして、2つの辞書からChainMapオブジェクトを作成する。
settings = ChainMap(user, base)
print(settings)
# 出力結果:
#ChainMap({'forecolor': 'black', 'backcolor': 'white', 'fontsize': 12},
#{'width': 80, 'height': 25, 'forecolor': 'white', 'backcolor': 'black'})
このときには、ベースラインの設定をユーザー設定で上書きしたいので、「ChainMap(user, base)」のような書き方になる。ChainMapオブジェクトのキーと値を反復すると、ユーザー設定でベースライン設定が上書きされていることが分かる。
for k, v in settings.items():
print(f'{k}: {v}')
# 出力結果:
#width: 80
#height: 25
#forecolor: black
#backcolor: white
#fontsize: 12
このChainMapオブジェクトに対する変更はユーザー設定に対して行われる。変更を保存したければ、辞書userをjson.dump関数で保存すればよいだろう。
Copyright© Digital Advantage Corp. All Rights Reserved.