こんな人にオススメ
list
は+
記号で配列の足し算(結合)ができるけど、dict
ってどうすれば結合できるの?list
とdict
で名前が似てるから同じく+
を使うの?それとも他の方法が必要?ということで、今回は執筆者のよく使っている
dict
のマージ(結合)について解説する。というのも、執筆者、大学院を修了するにあたってPCを新しく乗り換えた。それまでは2017年モデルのMacbook Proを使用していたのをM1 Macbook Proにし、python環境を再構築した。その際にpythonのバージョンがそれまでの3.8系ではなく3.9系を選んだ。これにより
dict
のマージに新たな機能として|
でのマージが使えるようになったのだ。
執筆者の人生概略については以下参照。 続きを見る 続きを見る
【M天パ(めがてんぱ)自己紹介】変わって楽しいの繰り返し
執筆者がM1Macbookに入れているアプリについては以下参照。
【M1 Mac】MacBook Proに入れている便利でニッチなアプリを21個紹介する
本記事ではこの|
を実際に使ってみたいという思いを発端に、色々なdict
のマージについて解説する。
python環境は以下。
- Python 3.9.2
dict
の基礎
ここではまずdict
とは、ということとざっくりとした使い方を解説する。
dict
とは
そもそもdict
とは何かということで、dict
とはざっというと
keys
とvalues
を紐づけることで値を管理する配列ということ。実際にコードで見ると以下のようなもの。
# dictの作成 dct = {'a': 10, 'b': [1, 2, 3], 'c': (10, 20, 30), 'd': 'd daze'} dct = dict(a=10, b=[1, 2, 3], c=(10, 20, 30), d='d daze') print(dct) # {'a': 10, 'b': [1, 2, 3], 'c': (10, 20, 30), 'd': 'd daze'}
2つの作成方法を書いたが、作り方についてはどちらの方法でも大丈夫。また、空のdict
に要素を追加することも可能。
# 要素の追加 dct['e'] = 'add!' print(dct) # {'a': 10, 'b': [1, 2, 3], 'c': (10, 20, 30), 'd': 'd daze', 'e': 'add!'}
要素の取り出し方は、'a'
とか'b'
は.keys()
で、10
とか[1, 2, 3]
とかは.values()
で可能。ただし、取り出した後の配列のtype
が特殊なのでlist
に変換するのが楽。また、keys
もvalues
も同時に取り出したい時は.items()
を使う。
# 要素の取り出し keys = dct.keys() values = dct.values() print(keys) # dict_keys(['a', 'b', 'c', 'd', 'e']) print(type(keys)) # <class 'dict_keys'> print(list(keys)) # ['a', 'b', 'c', 'd', 'e'] print(values) # dict_values([10, [1, 2, 3], (10, 20, 30), 'd daze', 'add!']) print(type(values)) # <class 'dict_values'> print(list(values)) # [10, [1, 2, 3], (10, 20, 30), 'd daze', 'add!'] items = dct.items() print(items) # dict_items([('a', 10), ('b', [1, 2, 3]), ('c', (10, 20, 30)), ('d', 'd daze'), ('e', 'add!')]) print(type(items)) # <class 'dict_items'> print(list(items)) # [('a', 10), ('b', [1, 2, 3]), ('c', (10, 20, 30)), ('d', 'd daze'), ('e', 'add!')]
for
ループでdct
のまま回すとkey
が取り出される。keys
もvalues
も取り出したい時は.items()
で両方回すのが楽。
# ループ処理 for k in dct: print(k) # a # b # c # d # e for k, v in dct.items(): print(f"keys: {k}, values: {v}") # keys: a, values: 10 # keys: b, values: [1, 2, 3] # keys: c, values: (10, 20, 30) # keys: d, values: d daze # keys: e, values: add!
dict
のマージとは
本記事のメインテーマであるdict
のマージとは簡単にいうと2つ以上のdict
を結合するということ。list
の場合だと単純に+
で足せばよかった。しかしdict
の場合はできない。ではどうすればいいのかというのが本記事の内容。
# listでのマージ lst1 = [1, 2, 3] lst2 = ['1', '2', '3'] lst1_2 = lst1 + lst2 print(lst1_2) # [1, 2, 3, '1', '2', '3'] # dictでのマージは+ではできない dct1 = {'a1': 1, 'b1': 10, 'c1': 100} dct2 = {'a2': 2, 'b2': 20, 'c2': 200} dct1_2 = dct1 + dct2 # TypeError: unsupported operand type(s) for +: 'dict' and 'dict'
dict
のマージ
今回dict
のマージとして使用するdict
は以下の5つ。dct1_same
はdct1
のkey
の一つ'a1'
を持つdict
で、上書きされたかどうかを確認する用。
# 今回使用する辞書の登場 dct1 = {'a1': 1, 'b1': 10, 'c1': 100} dct2 = {'a2': 2, 'b2': 20, 'c2': 200} dct3 = {'a3': 3, 'b3': 30, 'c3': 300} dct4 = {'a4': 4, 'b4': 40, 'c4': 300} # keysが被った時の動作を調べる用のdct1と同じkeysを持つ辞書 dct1_same = {'a1': 'Overwrite!'}
for
ループ
まずはシンプルにfor
ループで回してマージ。最終的なdict
のdct1_2
を空で作成し、その中に要素を入れていく感じ
# ループで回す dct1_2 = {} for key1 in dct1: dct1_2[key1] = dct1[key1] for key2 in dct2: dct1_2[key2] = dct2[key2] print(dct1_2) # {'a1': 1, 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200}
同じkeys
を要素として追加すると上書きされる。
# 同じkeysは上書きされる for key1_same in dct1_same: dct1_2[key1_same] = dct1_same[key1_same] print(dct1_2) # {'a1': 'Overwrite!', 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200}
.updateで破壊的にマージ
.update
を使うことでもマージできるが、この場合は使ったdict
が更新される、すなわち破壊的である。元のdict
を更新したい場合はいいが、元のdict
はそのままにしておきたい場合はこの方法はダミーを作成する必要がある。
# dct1が破壊されるので、犠牲になってくれるダミーを作成 dct1_dummy = {'a1': 1, 'b1': 10, 'c1': 100} dct1_dummy.update(dct2) print(dct1_dummy) # {'a1': 1, 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200} print(dct1) # {'a1': 1, 'b1': 10, 'c1': 100}
今回もfor
ループと同様、同じkeys
は上書きされる。dict1_dummy
は上書きされているので毎回ダミーを用意する。
# 同じkeysは上書きされる dct1_dummy = {'a1': 1, 'b1': 10, 'c1': 100} dct1_dummy.update(dct2) dct1_dummy.update(dct1_same) print(dct1_dummy) # {'a1': 'Overwrite!', 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200}
update
の中身を複数dict
にした場合はエラー。この場合はupdate
の中身でdict
を展開すれば問題ない。もちろん同じkeys
は上書きされる。
# 2つ同時はエラー # dct1_dummy = {'a1': 1, 'b1': 10, 'c1': 100} # dct1_dummy.update(dct2, dct3) # TypeError: update expected at most 1 argument, got 2 # 展開すればOK dct1_dummy = {'a1': 1, 'b1': 10, 'c1': 100} dct1_dummy.update(**dct2, **dct3) print(dct1_dummy) # {'a1': 1, 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200, 'a3': 3, 'b3': 30, 'c3': 300} # 同じkeysは上書きされる dct1_dummy = {'a1': 1, 'b1': 10, 'c1': 100} dct1_dummy.update(**dct2, **dct3, **dct1_same) print(dct1_dummy) # {'a1': 'Overwrite!', 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200, 'a3': 3, 'b3': 30, 'c3': 300}
update
内で展開している時に最後に展開していないdict
を入れるのはエラー。この場合は展開前にdict
を入れるか、入れたいdict
自身も展開するか。前者の場合は最後尾にはならないので注意。
# 最初に辞書を置くのはOK(順番が意図してものにならない) dct1_dummy = {'a1': 1, 'b1': 10, 'c1': 100} dct1_dummy.update({'ex': 1000}, **dct2, **dct3) print(dct1_dummy) # {'a1': 1, 'b1': 10, 'c1': 100, 'ex': 1000, 'a2': 2, 'b2': 20, 'c2': 200, 'a3': 3, 'b3': 30, 'c3': 300} # 展開してあげればOK dct1_dummy = {'a1': 1, 'b1': 10, 'c1': 100} dct1_dummy.update(**dct2, **dct3, **{'ex': 1000}) print(dct1_dummy) # 'a1': 1, 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200, 'a3': 3, 'b3': 30, 'c3': 300, 'ex': 1000}
dict
で直接展開
dict
の中で直接展開するのも一つの方法。この場合はdict()
でも{}
でもどちらでも動作する。 同じkeys
は上書きされる。3つ以上のdict
でも対応可能。
# 辞書内で展開してあげればOKかつ非破壊的 dct1_2 = dict(**dct1, **dct2) print(dct1_2) # {'a1': 1, 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200} dct1_2 = {**dct1, **dct2} print(dct1_2) # {'a1': 1, 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200} # 同じkeysは上書きされる dct1_2_1same = {**dct1, **dct2, **dct1_same} print(dct1_2) # {'a1': 1, 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200} # 3つ以上もOK dct1_2_3 = dict(**dct1, **dct2, **dct3) print(dct1_2_3) # {'a1': 1, 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200, 'a3': 3, 'b3': 30, 'c3': 300} dct1_2_3_4 = {**dct1, **dct2, **dct3, **dct4} print(dct1_2_3_4) # {'a1': 1, 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200, 'a3': 3, 'b3': 30, 'c3': 300, 'a4': 4, 'b4': 40, 'c4': 300}
|
を使ったマージ
さて、本記事を書こうと思ったきっかけになった|
を使ったdict
のマージ。こちらも前節の「dictで直接展開」と同様に並べて書くことができる。
# python3.9から|でマージ可能に dct1_2 = dct1 | dct2 print(dct1_2) # {'a1': 1, 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200} # 3つ以上もOK dct1_2_3 = dct1 | dct2 | dct3 print(dct1_2_3) # {'a1': 1, 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200, 'a3': 3, 'b3': 30, 'c3': 300} # 同じkeysは上書きされる dct1_2_1same = dct1 | dct2 | dct1_same print(dct1_2_1same) # {'a1': 'Overwrite!', 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200}
さらに|
の場合は代入演算子のように扱うこともできる。この場合もkeys
は上書きされる。
# 代入演算子っぽく扱うことも可能 dct1_dummy = {'a1': 1, 'b1': 10, 'c1': 100} dct1_dummy |= dct2 print(dct1_dummy) # {'a1': 1, 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200} # 同じkeysは上書きされる dct1_dummy = {'a1': 1, 'b1': 10, 'c1': 100} dct1_dummy |= dct1_2_1same print(dct1_dummy) # {'a1': 'Overwrite!', 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200}
右辺に2つ以上のdict
を入れた場合はエラー。この場合はdict
で展開すればOK。
# 右辺を2つ以上入れるとエラー # dct1_dummy = {'a1': 1, 'b1': 10, 'c1': 100} # dct1_dummy |= dct2, dct3 # ValueError: dictionary update sequence element #0 has length 3; 2 is required # 展開して入れればOK dct1_dummy = {'a1': 1, 'b1': 10, 'c1': 100} dct1_dummy |= {**dct2, **dct3} print(dct1_dummy) # {'a1': 1, 'b1': 10, 'c1': 100, 'a2': 2, 'b2': 20, 'c2': 200, 'a3': 3, 'b3': 30, 'c3': 300}
ChainMap
を使った方法
最後にマニアックな方法としてChainMap
を使用した方法について紹介する。ChainMap
は標準ライブラリのcollectioins
の中にある。単純にChainMap
を適用しただけだとネストされた辞書になるだけ。しかし、dict
の中に入れてあげればマージすることができる。ここで注意しないといけないのが、dict
に変換したときには順番がネストのdict
の並びの逆になるということ。今回ではdict1, 2, 3
の順番で作成したが、最終的な順番はdict3, 2, 1
の配列になっている。
from collections import ChainMap # ChainMapだけだとネストされたdict chainmap1_2_3 = ChainMap(dct1, dct2, dct3) print(chainmap1_2_3) # ChainMap({'a1': 1, 'b1': 10, 'c1': 100}, {'a2': 2, 'b2': 20, 'c2': 200}, {'a3': 3, 'b3': 30, 'c3': 300}) # typeは<class 'collections.ChainMap'> print(type(chainmap1_2_3)) # <class 'collections.ChainMap'> # dictでくくるとdictに変換できるが、並びが逆になる print(dict(chainmap1_2_3)) # {'a3': 3, 'b3': 30, 'c3': 300, 'a2': 2, 'b2': 20, 'c2': 200, 'a1': 1, 'b1': 10, 'c1': 100}
並び方が逆になるので、上書きされるときも右側のkey
ではなく左側のkey
が上書きされる。
# 同じkeysは左のkeys優先で上書きされる chainmap1_2_1same = ChainMap(dct1, dct2, dct1_same) print(chainmap1_2_1same) # ChainMap({'a1': 1, 'b1': 10, 'c1': 100}, {'a2': 2, 'b2': 20, 'c2': 200}, {'a1': 'Overwrite!'}) print(dict(chainmap1_2_1same)) # {'a1': 1, 'a2': 2, 'b2': 20, 'c2': 200, 'b1': 10, 'c1': 100} # この場合はdct1_sameのvaluesで上書きされる chainmap1_2_1same = ChainMap(dct1_same, dct1, dct2) print(chainmap1_2_1same) # ChainMap({'a1': 'Overwrite!'}, {'a1': 1, 'b1': 10, 'c1': 100}, {'a2': 2, 'b2': 20, 'c2': 200}) print(dict(chainmap1_2_1same)) # {'a2': 2, 'b2': 20, 'c2': 200, 'a1': 'Overwrite!', 'b1': 10, 'c1': 100}
なお、ChainMap
をdict
にする際にdict()
ではなく{}
を使うとsetと認識されてエラーになるので注意。
# {}だとsetに考えられると思うので、ハッシュ可能ではないということでエラー # print({chainmap1_2_1same})
最後に注意点。ChainMap
の状態で要素の変更をしてしまうと、元のdict
の変更した要素も変更されるので注意。
# ChainMapで要素を変更すると、元のdictの中身も変更される chainmap1_2_3['a1'] = 'Change!' print(chainmap1_2_3) # ChainMap({'a1': 'Change!', 'b1': 10, 'c1': 100}, {'a2': 2, 'b2': 20, 'c2': 200}, {'a3': 3, 'b3': 30, 'c3': 300}) print(dct1) # {'a1': 'Change!', 'b1': 10, 'c1': 100}
一度dict
に変更してしまえば変更されない。
# dictの状態で変更したら大丈夫 # 上書きされたので、再度dct1定義 dct1 = {'a1': 1, 'b1': 10, 'c1': 100} chainmap1_2_3 = ChainMap(dct1, dct2, dct3) # dictに変換する chainmap1_2_3todct = dict(chainmap1_2_3) print(chainmap1_2_3todct) # {'a3': 3, 'b3': 30, 'c3': 300, 'a2': 2, 'b2': 20, 'c2': 200, 'a1': 1, 'b1': 10, 'c1': 100} print(dct1) # {'a1': 1, 'b1': 10, 'c1': 100}
マージ方法には注意
最近、自分で書いているコードにdict
が多発している。基本的にはスペクトルデータを入れているが、そのスペクトルの名称をkeys
にしてスペクトルをvalues
にするという感じだ。作成したスペクトルの入ったdictをマージする際にはこれからは|
を使ってみようかなと思う。
ただし、python3.9から導入された方法なので他の環境で使う可能性があるのであれば注意が必要だ。