こんな人にオススメ
dict
とやらに手を出してすごい便利だと歓喜しているのですが、ネストされた時のkeys
の指定が面倒です。例えば以下のような例。
dct = { 1: { 'a': [1, 2, 3], 'b': (10, 20, 30), }, } print(dct) # {1: {'a': [1, 2, 3], 'b': (10, 20, 30)}} print(dct[1]['a']) # [1, 2, 3]
毎回毎回、欲しいkeys
をこまめにコピペするのが面倒です。すぐにコピペできるようないいコード、ありませんかね?
ということで、今回はdict
のkeys
をネストごとに並べて、さらにすぐにvalues
を取得できるようにしたコードを紹介する。このコードを完成させたときはなかなか感激した。
コード自体はややこしいが超絶便利だったので、一度使っていただきたい。
完成イメージは以下の通り。
dct = { 1: { 'a': [1, 2, 3], 'b': (10, 20, 30), }, } print(allkeys(dct)) # [1, "[1]['a']", "[1]['b']"]
python環境は以下。
- Python 3.9.2
[alert title="注意"]
dict
の並び順の保存はPython 3.7以降からなので、3.7より古いバージョンを使用している環境では本記事の通りに動作するとは限りません!
[/alert]
使用するdict
と今までの取得方法
ここでは今回使用するdictと、本記事で紹介するコードを使用せずにコピペした場合はどのようにコピペするのかについて解説する。
使用するdict
今回使用するdict
は以下のもの。色々な型を使用したのでめちゃくちゃ長くなってしまった。
dct = { # 文字列のkeysでネスト # 同じ言葉でも小文字大文字で区別される 'a': dict( komoji='komoji', OOMOJI='OOMOJI', oomoji='iya kore komoji', ), # intのkeysでネスト # int, float違いで同じ数字だと先に定義したkeysが優先される # でもその奥のネストは後に定義したkeysが優先される 1: { 10: {'int': 1}, 10.0: {'float': 1.1}, 20.: {'float': 2.2}, }, # intの文字列のkeysでネスト # この場合は10と10.0は区別される '1': { '10': {'int': 1}, '10.0': {'float': 1.1}, '20.': {'float': 2.2}, }, # tupleのkeysでネスト # tupleとintだと区別される (100, 200, 300): { (1): 'Kore ha int', (1,): 'Kore ha tuple', }, # boolをkeysでネスト # Trueにすると既にkeysで1を選んでいるので、ダブって使えない # (エラーは出ないが読み込まれない) False: { 'F': False, True: True }, } print(dct) # {'a': {'komoji': 'komoji', 'OOMOJI': 'OOMOJI', 'oomoji': 'iya kore komoji'}, 1: {10: {'float': 1.1}, 20.0: {'float': 2.2}}, '1': {'10': {'int': 1}, '10.0': {'float': 1.1}, '20.': {'float': 2.2}}, (100, 200, 300): {1: 'Kore ha int', (1,): 'Kore ha tuple'}, False: {'F': False, True: True}}
一番浅いネストのkeys
は'a'
, 1
, '1'
, (100, 200, 300)
, False
の5種類。これら5種類のkeys
の中にさらにdict
が入っている。
初めのkeys
の'a'
では小文字と大文字の文字列をネストのdict
のkeys
に設定している。ここで同じ文字列のkeys
でも小文字か大文字かが違っていれば別のkeys
として認識される。
# 文字列のkeysでネスト # 同じ言葉でも小文字大文字で区別される 'a': dict( komoji='komoji', OOMOJI='OOMOJI', oomoji='iya kore komoji', ),
次のkeys
の1
についてだが、これが奇妙な挙動を示す。一応keys
としてはint
の10
とfloat
の10.0
に設定しているが、これらは同じとみなされて、初めの10
がkeys
として採用される。しかし、その奥のネストでは反対に10.0
のkeys
である'float'
が採用される。
なんかよくわからんが、今回はこれについては置いておく。そういうもんなんだろう。
# intのkeysでネスト # int, float違いで同じ数字だと先に定義したkeysが優先される # でもその奥のネストは後に定義したkeysが優先される 1: { 10: {'int': 1}, 10.0: {'float': 1.1}, 20.: {'float': 2.2}, },
一方で、文字列として定義すると、'10'
も'10.0'
も別々のkeys
として認識され、それぞれのdict
が出力される。
# intの文字列のkeysでネスト # この場合は10と10.0は区別される '1': { '10': {'int': 1}, '10.0': {'float': 1.1}, '20.': {'float': 2.2}, },
次はtuple
のkeys
である(100, 200, 300)
。実はdict
のkeys
は変更可能なものを設定することができない。tuple
は中身を変更できない配列だから採用可能であるが、list
の場合は変更可能なのでdict
のkeys
として設定することができない。
これはdict
のkeys
が途中で変更されるのを防ぐためだ。途中で変更されてしまったら、そのkeys
のvalues
の行方はどうなるのかという問題になる。
# tupleのkeysでネスト # tupleとintだと区別される (100, 200, 300): { (1): 'Kore ha int', (1,): 'Kore ha tuple', }, # listは中身を変更可能 lst = [1, 2, 3] print(lst) # [1, 2, 3] lst[1] = 100 print(lst) # [1, 100, 3] # tupleは中身を変更不可能 tpl = (1, 2, 3) print(tpl) # [1, 2, 3] tpl[1] = 100 # tpl[1] = 100 # TypeError: 'tuple' object does not support item assignment
最後はbool
であるFalse
。なぜFalse
にしたかというと、True
の場合は数値としては1
という判定を喰らうので(False
は0
)、既に定義したkeys
の1
と被るから。被った場合は正しくkeys
として採用されなくなってしまう。
# boolをkeysでネスト # Trueにすると既にkeysで1を選んでいるので、ダブって使えない # (エラーは出ないが読み込まれない) False: { 'F': False, True: True },
これらの要素から出力されるdict
、dct
は以下の通り。
print(dct) # {'a': {'komoji': 'komoji', 'OOMOJI': 'OOMOJI', 'oomoji': 'iya kore komoji'}, 1: {10: {'float': 1.1}, 20.0: {'float': 2.2}}, '1': {'10': {'int': 1}, '10.0': {'float': 1.1}, '20.': {'float': 2.2}}, (100, 200, 300): {1: 'Kore ha int', (1,): 'Kore ha tuple'}, False: {'F': False, True: True}}
この配列の中から任意のネストの要素を引き出す必要がある。
今までのネストの取得
dct
を以上で定義したが、任意の要素を取得したい場合は以下のように書くのが一般的だと思う。
print(dct['a']['komoji']) # komoji print(dct[1][20.0]) # {'float': 2.2} print(dct[1][20.0]['float']) # 2.2 print(dct[False]) # {'F': False, True: True}
どの要素が欲しいのかを辞書から見つけ出し、そのネストに従って[]
を使って指定する。しかし、dct
の出力からわかるように、ネストの構造は分かりやすくてもコピペするのが面倒。一つの[]
につき1回のコピペが必要になるので労力が半端ない。
そこで、本記事で作成したコードで楽をしようというわけ。次の章から解説していく。
ネストされたdict
のkeys
を[]
付きで出力
さて、本題のdict
のkeys
の出力変更についてだが、今回は以下の2サイトを参考させていただいた。とても分かりやすく、かつ、本記事のコードを完成させるのには必要不可欠の内容だった。
このサイトでは本記事の内容の大枠を参考にさせていただいた。
このサイトでは一度に複数の文字列を置換する操作について参考にさせていただいた。
全体コード
ブロックとしては以下の4章構成。
dいct
の定義:dct
- 余分なクォーテーションを削除:
decide
- 余分なかっことクォーテーションを削除:
multiple_replace
- 2., 3. と組み込んで、ネストされた
dict
のkeys
をカッコを使用して出力する:allkeys
それぞれ解説する。
余分なクォーテーションを削除:decide
def decide(object): """入力された内容が文字列の場合は、余計なクォーテーションを削除 Parameters ---------- object : any 余計なクォーテーションを削除した内容 strの時だけ操作する Returns ------- any strの場合のみクォーテーションの削除を行った後の入力内容 """ if type(object) is str: # 追加されるクォーテーションを削除 object = "'{}'".format(object.replace('\\'\\'', '\\'')) return object
後ほど説明するが、入力したkeys
が文字列の場合はクォーテーション'
をつけて文字列であることを明示したい。例えば1
と'1'
をどちらも1
と表現してしまった場合、keys
としてどちらを選択すればいいのかわからないからだ。
そのためにdecide
関数を使用した。まずは"'{}'"
でクォーテーションを追加。しかし、既に前のループでクォーテーションが追加されている状態だと、''a''
や'''a'''
のように余計なクォーテーションがついてしまう。
そこで''
を'
に置き換えることで、余計なクォーテーションを削除している。
余分なかっことクォーテーションを削除:multiple_replace
def multiple_replace(text, adict): """ 一度に複数の文字列を置換する. text中からdictのキーに合致する文字列を探し、対応の値で置換して返す <http://omoplatta.blogspot.com/2010/10/python_30.html> """ # マッチさせたいキー群を正規表現の形にする e.g) (a1|a2|a3...) rx = re.compile('|'.join(map(re.escape, adict))) def one_xlat(match): return adict[match.group(0)] return rx.sub(one_xlat, text)
しかし、ループを重ねるごとに[]
が追加され、''
を'
に置き換えるだけではうまくいかない場面が出てくる。例えば"[''1'']['['10']['int']']"
のように'[
や]'
が余計についてくることがある。それを避けるためにmultiple_replace関数で]']
を]
に、['[
を[
に、そして''
を'
に変換することで、カッコの形式を整形した。
ネストされたdict
のkeys
をカッコを使用して出力する:allkeys
def allkeys(dct): """ネストになっているdictの全てのkeyを出力する <https://qiita.com/lnial/items/e898b7bc8bfc4afdb445> Parameters ---------- dct : dict keyを取得したいdict Returns ------- list ネスト部分も含めたkeyの名称 Notes ----- pythonのバージョンによるが、dictの順番が保たれるのであればネストの浅いkeyから順に並ぶはず ネスト部分は浅い方から深い方へ「[浅い][深い]」の形式で出力される 文字列のkeysはクォーテーション「''」つきで、数値のkeysは「''」なしで出力される """ keys = list(dct.keys()) for parent, children in dct.items(): if isinstance(children, dict): # 子がさらにdictかどうかの判定 for child in allkeys(children): parent = decide(parent) child = decide(child) # 親の親なども含んだ親が無駄に['']をつけないように削除 # &最初のkeysの1つ多い「''」を削除 adict = {']\\']': ']', '[\\'[': '[', '\\'\\'': '\\''} append = multiple_replace(f"[{parent}][{child}]", adict) keys.append(append) return keys
最後はこれらの関数を使用しつつ、入力値がdict
である限りkeys
をカッコ[]
に入れる関数allkeys
。
isinstance(children, dict)
でより深いネストのchildren
がdict
であるかを判定。dict
であるなら最後allkeys
を実行してネストの最奥まで見ていく。この過程で先程のdecide
関数とmultiple_replace
関数を使用いている。
allkeys
関数を使用して得られる出力は以下の通り。
print(allkeys(dct)) # ['a', 1, '1', (100, 200, 300), False, "['a']['komoji']", "['a']['OOMOJI']", "['a']['oomoji']", '[1][10]', '[1][20.0]', "[1][10]['float']", "[1][20.0]['float']", "['1']['10']", "['1']['10.0']", "['1']['20.']", "['1']['10']['int']", "['1']['10.0']['float']", "['1']['20.']['float']", '[(100, 200, 300)][1]', '[(100, 200, 300)][(1,)]', "[False]['F']", '[False][True]']
文字列のであるkeys
は文字列のままで、数値であるkeys
は数値のまま、というように、元のdict
のkeys
に沿った形式なっていることがわかる。
この配列から任意の要素をコピペするだけで簡単に要素の抽出を行うことができる。例えば以下のように抽出することができる。
print(dct['1']['10.0']['float']) # 1.1 print(dct[(100, 200, 300)]) # {1: 'Kore ha int', (1,): 'Kore ha tuple'} print(dct[(100, 200, 300)][(1,)]) # Kore ha tuple
うまくいっている。また、allkeys
を出力する際に縦に並ぶように設定するとより分かりやすい。ただし、この場合は一番浅いネストのkeys
の1
と'1'
が同じ1
として出力されているので注意が必要だ。
print(*allkeys(dct), sep='\\n') # a # 1 # 1 # (100, 200, 300) # False # ['a']['komoji'] # ['a']['OOMOJI'] # ['a']['oomoji'] # [1][10] # [1][20.0] # [1][10]['float'] # [1][20.0]['float'] # ['1']['10'] # ['1']['10.0'] # ['1']['20.'] # ['1']['10']['int'] # ['1']['10.0']['float'] # ['1']['20.']['float'] # [(100, 200, 300)][1] # [(100, 200, 300)][(1,)] # [False]['F'] # [False][True]
失敗例
ここからはコードの失敗例を挙げていく。動くのは動くが、思うような結果が得られないものだ。
失敗例1(decide
, multiple_replace
を使わない)
まずはdecide
, multiple_replace
を使わない場合。
この場合は文字列の部分にクォーテーションがついていないので、
1
と’1’
の区別がつかなかったり、文字列なのに変数と勘違いしてしまう可能性がある。
失敗例2(multiple_replace
を使わない)
multiple_replace
を使わない場合は、クォーテーションが余計についてしまいこのままコピペすることができない。
失敗例3(decide
を使わない)
最後は
decide
を使わない場合。この場合は文字にクォーテーションがつかないだけではなく、カッコが余計についている部分がある。
ネスト状況の一覧性向上と楽なコピペ
今回はネストされたdict
のkeys
を、実際の要素出力に模して出力するコードを作成した。このコードがあれば、どのkeys
がどこに所属しているのかもわかるし、実際に出力する際にもコピペが楽になる。
執筆者自身、このコードはよく使っているので、皆様も一度使用してみてはいかがでしょうか。
関連記事
-
-
【辞書の結合】dictのマージ
続きを見る
-
-
【辞書&pandas】dict{name: , val: {a: [~], b:[~]}}のpandas化
続きを見る