カテゴリー

Python基礎

【python3.7以降&dictのkeys】ネスト(入れ子)されたdictのkeys一覧をカッコで出力

2021年5月9日

こんな人にオススメ

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をこまめにコピペするのが面倒です。すぐにコピペできるようないいコード、ありませんかね?

ということで、今回はdictkeysをネストごとに並べて、さらにすぐに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]

スポンサーリンク
スポンサーリンク

運営者のメガネです。TwitterInstagramも運営してます。

自己紹介はこちらから、お問い合わせはこちら。

運営者メガネ

使用する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'では小文字と大文字の文字列をネストのdictkeysに設定している。ここで同じ文字列のkeysでも小文字か大文字かが違っていれば別のkeysとして認識される。

# 文字列のkeysでネスト
# 同じ言葉でも小文字大文字で区別される
'a': dict(
    komoji='komoji',
    OOMOJI='OOMOJI',
    oomoji='iya kore komoji',
),

次のkeys1についてだが、これが奇妙な挙動を示す。一応keysとしてはint10float10.0に設定しているが、これらは同じとみなされて、初めの10keysとして採用される。しかし、その奥のネストでは反対に10.0keysである'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},
},

次はtuplekeysである(100, 200, 300)。実はdictkeysは変更可能なものを設定することができない。tupleは中身を変更できない配列だから採用可能であるが、listの場合は変更可能なのでdictkeysとして設定することができない。

これはdictkeysが途中で変更されるのを防ぐためだ。途中で変更されてしまったら、そのkeysvaluesの行方はどうなるのかという問題になる。

# 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という判定を喰らうので(False0)、既に定義したkeys1と被るから。被った場合は正しくkeysとして採用されなくなってしまう。

# boolをkeysでネスト
# Trueにすると既にkeysで1を選んでいるので、ダブって使えない
# (エラーは出ないが読み込まれない)
False: {
    'F': False,
    True: True
},

これらの要素から出力されるdictdctは以下の通り。

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回のコピペが必要になるので労力が半端ない。

そこで、本記事で作成したコードで楽をしようというわけ。次の章から解説していく。

ネストされたdictkeys[]付きで出力

さて、本題のdictkeysの出力変更についてだが、今回は以下の2サイトを参考させていただいた。とても分かりやすく、かつ、本記事のコードを完成させるのには必要不可欠の内容だった。

このサイトでは本記事の内容の大枠を参考にさせていただいた。

このサイトでは一度に複数の文字列を置換する操作について参考にさせていただいた。

全体コード

ブロックとしては以下の4章構成。

  1. dいctの定義:dct
  2. 余分なクォーテーションを削除:decide
  3. 余分なかっことクォーテーションを削除:multiple_replace
  4. 2., 3. と組み込んで、ネストされたdictkeysをカッコを使用して出力する: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関数で]']]に、['[[に、そして'''に変換することで、カッコの形式を整形した。

ネストされたdictkeysをカッコを使用して出力する: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)でより深いネストのchildrendictであるかを判定。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は数値のまま、というように、元のdictkeysに沿った形式なっていることがわかる。

この配列から任意の要素をコピペするだけで簡単に要素の抽出を行うことができる。例えば以下のように抽出することができる。

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を出力する際に縦に並ぶように設定するとより分かりやすい。ただし、この場合は一番浅いネストのkeys1'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を使わない場合。この場合は文字にクォーテーションがつかないだけではなく、カッコが余計についている部分がある。

ネスト状況の一覧性向上と楽なコピペ

今回はネストされたdictkeysを、実際の要素出力に模して出力するコードを作成した。このコードがあれば、どのkeysがどこに所属しているのかもわかるし、実際に出力する際にもコピペが楽になる。

執筆者自身、このコードはよく使っているので、皆様も一度使用してみてはいかがでしょうか。

関連記事

【辞書の結合】dictのマージ

続きを見る

多項式関数のグラフ
【辞書&pandas】dict{name: , val: {a: [~], b:[~]}}のpandas化

続きを見る

関連コンテンツ

スポンサーリンク

Amazonのお買い物で損したない人へ

1回のチャージ金額通常会員プライム会員
¥90,000〜2.0%2.5%
¥40,000〜1.5%2.0%
¥20,000〜1.0%1.5%
¥5,000〜0.5%1.0%

Amazonギフト券にチャージすることでお得にお買い物できる。通常のAmazon会員なら最大2.0%、プライム会員なら2.5%還元なのでバカにならない。

ゲットしたポイントは通常のAmazonでのお買い物に使えるからお得だ。一度チャージしてしまえば、好きなタイミングでお買いものできる。

なお、有効期限は10年だから安心だ。いつでも気軽にAmazonでお買い物できる。

Amazonチャージはここから出来るで

もっとお得なAmazon Prime会員はこちらから

30日間無料登録

執筆者も便利に使わせてもらってる

スポンサーリンク

  • この記事を書いた人

メガネ

独学でpythonを学び天文学系の大学院を修了。 ガジェット好きでMac×Android使い。色んなスマホやイヤホンを購入したいけどお金がなさすぎて困窮中。 元々、人見知りで根暗だったけど、人生楽しもうと思って良い方向に狂ったために今も人生めちゃくちゃ楽しい。 pythonとガジェットをメインにブログを書いていますので、興味を持たれましたらちょこちょこ訪問してくだされば幸いです🥰。 自己紹介→変わって楽しいの繰り返し

-Python基礎
-