キーのリストを介してネストされた辞書項目にアクセスしますか?


143

正しい項目をアドレス指定するためのキーのリストを介してアクセスしたい複雑な辞書構造があります。

dataDict = {
    "a":{
        "r": 1,
        "s": 2,
        "t": 3
        },
    "b":{
        "u": 1,
        "v": {
            "x": 1,
            "y": 2,
            "z": 3
        },
        "w": 3
        }
}    

maplist = ["a", "r"]

または

maplist = ["b", "v", "y"]

私は機能する次のコードを作成しましたが、誰かがアイデアを持っている場合、これを行うためのより良い、より効率的な方法があると確信しています。

# Get a given data from a dictionary with position provided as a list
def getFromDict(dataDict, mapList):    
    for k in mapList: dataDict = dataDict[k]
    return dataDict

# Set a given data in a dictionary with position provided as a list
def setInDict(dataDict, mapList, value): 
    for k in mapList[:-1]: dataDict = dataDict[k]
    dataDict[mapList[-1]] = value

回答:


230

reduce()辞書をトラバースするために使用します。

from functools import reduce  # forward compatibility for Python 3
import operator

def getFromDict(dataDict, mapList):
    return reduce(operator.getitem, mapList, dataDict)

そしてgetFromDict、値を保存する場所を見つけるために再利用しますsetInDict()

def setInDict(dataDict, mapList, value):
    getFromDict(dataDict, mapList[:-1])[mapList[-1]] = value

最後の要素を除くすべての要素はmapList、値を追加する「親」ディクショナリを見つけるために必要です。次に、最後の要素を使用して、値を正しいキーに設定します。

デモ:

>>> getFromDict(dataDict, ["a", "r"])
1
>>> getFromDict(dataDict, ["b", "v", "y"])
2
>>> setInDict(dataDict, ["b", "v", "w"], 4)
>>> import pprint
>>> pprint.pprint(dataDict)
{'a': {'r': 1, 's': 2, 't': 3},
 'b': {'u': 1, 'v': {'w': 4, 'x': 1, 'y': 2, 'z': 3}, 'w': 3}}

Python PEP8スタイルガイドでは、関数のsnake_case名が規定されていることに注意してください。上記はリストや辞書とリストの組み合わせでも同じように機能するため、名前は実際に次のようにget_by_path()なりset_by_path()ます。

from functools import reduce  # forward compatibility for Python 3
import operator

def get_by_path(root, items):
    """Access a nested object in root by item sequence."""
    return reduce(operator.getitem, items, root)

def set_by_path(root, items, value):
    """Set a value in a nested object in root by item sequence."""
    get_by_path(root, items[:-1])[items[-1]] = value

1
このようなトラバースは、任意の入れ子構造に対してどの程度信頼できますか?ネストされたリストを含む混合辞書でも機能しますか?getFromDict()を変更してdefault_valueを提供し、デフォルトのdefault_valueをNoneにするにはどうすればよいですか?私はPythonの初心者で、長年のPHP開発とC開発の前の経験があります。
Dmitriy Sintsov 2015

2
また、ネストされたマップセットは、存在しないノード、imo:整数キーのリスト、文字列キーの辞書を作成する必要があります。
Dmitriy Sintsov 2015

1
@ user1353510:通常、ここでは通常のインデックス構文が使用されているため、辞書内のリストもサポートされます。それらの整数インデックスを渡すだけです。
Martijn Pieters

1
@ user1353510:デフォルト値、使用するためにtry:except (KeyError, IndexError): return default_value現在の周りのreturnライン。
Martijn Pieters

1
@Georgy:使用dict.get()するとセマンティクスが変更されます。欠落した名前に対して発生するのではNoneなく、戻るからKeyErrorです。その後、後続の名前はをトリガーしAttributeErrorます。operator標準ライブラリです。ここでそれを回避する必要はありません。
Martijn Pieters

40
  1. 承認されたソリューションはpython3では直接機能しません-が必要になりますfrom functools import reduce
  2. また、forループを使用する方がPythonicのようです。What's New In Python 3.0からの引用を参照してください。

    削除されましたreduce()functools.reduce()本当に必要な場合に使用してください。ただし、99%の確率で明示的なforループが読みやすくなります。

  3. 次に、受け入れられたソリューションは、存在しないネストされたキーを設定しません(それはを返しますKeyError)-ソリューションについては@eafitの回答を参照してください

だから、値を取得するためにkolergyの質問から提案された方法を使用しないのはなぜですか:

def getFromDict(dataDict, mapList):    
    for k in mapList: dataDict = dataDict[k]
    return dataDict

そして、値を設定するための@eafitの答えからのコード:

def nested_set(dic, keys, value):
    for key in keys[:-1]:
        dic = dic.setdefault(key, {})
    dic[keys[-1]] = value

どちらもpython 2および3で直接動作します


6
私はこの解決策を好みますが、注意してください。私が間違っていないのであれば、Python辞書は不変ではないのでgetFromDict、呼び出し元のを破壊する可能性がありdataDictます。私はだろうcopy.deepcopy(dataDict)最初。もちろん、(書かれているとおり)この動作は2番目の関数で必要です。
ディランF

15

reduceの使用は賢いですが、親キーがネストされた辞書に事前に存在しない場合、OPのsetメソッドに問題がある可能性があります。これは私がグーグル検索でこの主題について見た最初のSO投稿なので、少し良くしたいと思います。

インデックスと値のリストを指定して、ネストされたPython辞書の値を設定する)のsetメソッドは、欠落している親キーに対してより堅牢に見えます。コピーするには:

def nested_set(dic, keys, value):
    for key in keys[:-1]:
        dic = dic.setdefault(key, {})
    dic[keys[-1]] = value

また、キーツリーをたどり、すべての絶対キーパスを取得するメソッドを用意すると便利です。

def keysInDict(dataDict, parent=[]):
    if not isinstance(dataDict, dict):
        return [tuple(parent)]
    else:
        return reduce(list.__add__, 
            [keysInDict(v,parent+[k]) for k,v in dataDict.items()], [])

これの1つの使用法は、次のコードを使用して、ネストされたツリーをpandas DataFrameに変換することです(ネストされたディクショナリのすべての葉が同じ深さであると想定)。

def dict_to_df(dataDict):
    ret = []
    for k in keysInDict(dataDict):
        v = np.array( getFromDict(dataDict, k), )
        v = pd.DataFrame(v)
        v.columns = pd.MultiIndex.from_product(list(k) + [v.columns])
        ret.append(v)
    return reduce(pd.DataFrame.join, ret)

なぜ 'keys'引数の長さを2以上に制限するのnested_setですか?
alancalvitti

10

このライブラリは役に立つかもしれません:https : //github.com/akesterson/dpath-python

/ slashed / paths ala xpath経由で辞書にアクセスして検索するためのPythonライブラリ

基本的に、それはまるでファイルシステムであるかのように辞書をグロブすることができます。


3

再帰関数を使用するのはどうですか?

値を取得するには:

def getFromDict(dataDict, maplist):
    first, rest = maplist[0], maplist[1:]

    if rest: 
        # if `rest` is not empty, run the function recursively
        return getFromDict(dataDict[first], rest)
    else:
        return dataDict[first]

そして値を設定するには:

def setInDict(dataDict, maplist, value):
    first, rest = maplist[0], maplist[1:]

    if rest:
        try:
            if not isinstance(dataDict[first], dict):
                # if the key is not a dict, then make it a dict
                dataDict[first] = {}
        except KeyError:
            # if key doesn't exist, create one
            dataDict[first] = {}

        setInDict(dataDict[first], rest, value)
    else:
        dataDict[first] = value

2

インポートなしの純粋なPythonスタイル:

def nested_set(element, value, *keys):
    if type(element) is not dict:
        raise AttributeError('nested_set() expects dict as first argument.')
    if len(keys) < 2:
        raise AttributeError('nested_set() expects at least three arguments, not enough given.')

    _keys = keys[:-1]
    _element = element
    for key in _keys:
        _element = _element[key]
    _element[keys[-1]] = value

example = {"foo": { "bar": { "baz": "ok" } } }
keys = ['foo', 'bar']
nested_set(example, "yay", *keys)
print(example)

出力

{'foo': {'bar': 'yay'}}

2

キーの1つが存在しない場合にエラーを発生させたくない場合の代替方法(メインコードを中断せずに実行できるようにするため):

def get_value(self,your_dict,*keys):
    curr_dict_ = your_dict
    for k in keys:
        v = curr_dict.get(k,None)
        if v is None:
            break
        if isinstance(v,dict):
            curr_dict = v
    return v

この場合、入力キーのいずれかが存在しない場合、Noneが返されます。これは、メインコードで別のタスクを実行するためのチェックとして使用できます。


1

値を調べるたびにパフォーマンスヒットをとるのではなく、一度辞書を平坦化してから、次のようにキーを調べるだけです。 b:v:y

def flatten(mydict):
  new_dict = {}
  for key,value in mydict.items():
    if type(value) == dict:
      _dict = {':'.join([key, _key]):_value for _key, _value in flatten(value).items()}
      new_dict.update(_dict)
    else:
      new_dict[key]=value
  return new_dict

dataDict = {
"a":{
    "r": 1,
    "s": 2,
    "t": 3
    },
"b":{
    "u": 1,
    "v": {
        "x": 1,
        "y": 2,
        "z": 3
    },
    "w": 3
    }
}    

flat_dict = flatten(dataDict)
print flat_dict
{'b:w': 3, 'b:u': 1, 'b:v:y': 2, 'b:v:x': 1, 'b:v:z': 3, 'a:r': 1, 'a:s': 2, 'a:t': 3}

この方法flat_dict['b:v:y']では、を使用してアイテムを簡単に検索できます1

そして、ルックアップごとにディクショナリを走査する代わりに、ディクショナリをフラット化して出力を保存することでこれを高速化できるため、コールドスタートからのルックアップはフラット化されたディクショナリをロードし、単純にキー/値ルックアップを実行しないことを意味します。トラバーサル。


1

これを再帰で解決しました:

def get(d,l):
    if len(l)==1: return d[l[0]]
    return get(d[l[0]],l[1:])

あなたの例を使用して:

dataDict = {
    "a":{
        "r": 1,
        "s": 2,
        "t": 3
        },
    "b":{
        "u": 1,
        "v": {
            "x": 1,
            "y": 2,
            "z": 3
        },
        "w": 3
        }
}
maplist1 = ["a", "r"]
maplist2 = ["b", "v", "y"]
print(get(dataDict, maplist1)) # 1
print(get(dataDict, maplist2)) # 2

1

すべてのインデックスを2回処理せずにdict要素をチェックして設定してみませんか?

解決:

def nested_yield(nested, keys_list):
    """
    Get current nested data by send(None) method. Allows change it to Value by calling send(Value) next time
    :param nested: list or dict of lists or dicts
    :param keys_list: list of indexes/keys
    """
    if not len(keys_list):  # assign to 1st level list
        if isinstance(nested, list):
            while True:
                nested[:] = yield nested
        else:
            raise IndexError('Only lists can take element without key')


    last_key = keys_list.pop()
    for key in keys_list:
        nested = nested[key]

    while True:
        try:
            nested[last_key] = yield nested[last_key]
        except IndexError as e:
            print('no index {} in {}'.format(last_key, nested))
            yield None

ワークフローの例:

ny = nested_yield(nested_dict, nested_address)
data_element = ny.send(None)
if data_element:
    # process element
    ...
else:
    # extend/update nested data
    ny.send(new_data_element)
    ...
ny.close()

テスト

>>> cfg= {'Options': [[1,[0]],[2,[4,[8,16]]],[3,[9]]]}
    ny = nested_yield(cfg, ['Options',1,1,1])
    ny.send(None)
[8, 16]
>>> ny.send('Hello!')
'Hello!'
>>> cfg
{'Options': [[1, [0]], [2, [4, 'Hello!']], [3, [9]]]}
>>> ny.close()

1

パーティーには非常に遅くなりますが、これが将来誰かを助ける可能性がある場合に備えて投稿します。私の使用例では、次の関数が最もよく機能しました。辞書から任意のデータ型を取り出すように機能します

dictは、値を含む辞書です

リストは私たちの価値への「ステップ」のリストです

def getnestedvalue(dict, list):

    length = len(list)
    try:
        for depth, key in enumerate(list):
            if depth == length - 1:
                output = dict[key]
                return output
            dict = dict[key]
    except (KeyError, TypeError):
        return None

    return None

1

ネストされた属性を設定および取得するための2つの静的メソッドがあることに対するこれらの回答を見て満足です。これらのソリューションは、ネストされたツリーを使用するよりもはるかに優れていますhttps://gist.github.com/hrldcpr/2012250

これが私の実装です。

使用法

ネストされた属性呼び出しを設定するには sattr(my_dict, 1, 2, 3, 5) is equal to my_dict[1][2][3][4]=5

ネストされた属性呼び出しを取得するには gattr(my_dict, 1, 2)

def gattr(d, *attrs):
    """
    This method receives a dict and list of attributes to return the innermost value of the give dict       
    """
    try:
        for at in attrs:
            d = d[at]
        return d
    except(KeyError, TypeError):
        return None


def sattr(d, *attrs):
    """
    Adds "val" to dict in the hierarchy mentioned via *attrs
    For ex:
    sattr(animals, "cat", "leg","fingers", 4) is equivalent to animals["cat"]["leg"]["fingers"]=4
    This method creates necessary objects until it reaches the final depth
    This behaviour is also known as autovivification and plenty of implementation are around
    This implementation addresses the corner case of replacing existing primitives
    https://gist.github.com/hrldcpr/2012250#gistcomment-1779319
    """
    for attr in attrs[:-2]:
        if type(d.get(attr)) is not dict:
            d[attr] = {}
        d = d[attr]
    d[attrs[-2]] = attrs[-1]

1

python-benedictキーパスを使用してネストされたアイテムにアクセスするために使用することをお勧めします。

を使用してインストールしますpip

pip install python-benedict

次に:

from benedict import benedict

dataDict = benedict({
    "a":{
        "r": 1,
        "s": 2,
        "t": 3,
    },
    "b":{
        "u": 1,
        "v": {
            "x": 1,
            "y": 2,
            "z": 3,
        },
        "w": 3,
    },
}) 

print(dataDict['a.r'])
# or
print(dataDict['a', 'r'])

ここで完全なドキュメント:https : //github.com/fabiocaccamo/python-benedict


0

ネストされたリストや辞書を含む任意のjsonを使用して、無効なルックアップパスを適切に処理する機能も必要な場合は、次の解決策があります。

from functools import reduce


def get_furthest(s, path):
    '''
    Gets the furthest value along a given key path in a subscriptable structure.

    subscriptable, list -> any
    :param s: the subscriptable structure to examine
    :param path: the lookup path to follow
    :return: a tuple of the value at the furthest valid key, and whether the full path is valid
    '''

    def step_key(acc, key):
        s = acc[0]
        if isinstance(s, str):
            return (s, False)
        try:
            return (s[key], acc[1])
        except LookupError:
            return (s, False)

    return reduce(step_key, path, (s, True))


def get_val(s, path):
    val, successful = get_furthest(s, path)
    if successful:
        return val
    else:
        raise LookupError('Invalid lookup path: {}'.format(path))


def set_val(s, path, value):
    get_val(s, path[:-1])[path[-1]] = value

0

文字列を連結する方法:

def get_sub_object_from_path(dict_name, map_list):
    for i in map_list:
        _string = "['%s']" % i
        dict_name += _string
    value = eval(dict_name)
    return value
#Sample:
_dict = {'new': 'person', 'time': {'for': 'one'}}
map_list = ['time', 'for']
print get_sub_object_from_path("_dict",map_list)
#Output:
#one

0

@DomTomCatおよびその他のアプローチを拡張して、これらの機能(つまり、入力に影響を与えることなく、deepcopyを介して変更されたデータを返す)セッターおよびマッパーは、ネストされたdictおよびに対して機能しlistます。

セッター:

def set_at_path(data0, keys, value):
    data = deepcopy(data0)
    if len(keys)>1:
        if isinstance(data,dict):
            return {k:(set_by_path(v,keys[1:],value) if k==keys[0] else v) for k,v in data.items()}
        if isinstance(data,list):
            return [set_by_path(x[1],keys[1:],value) if x[0]==keys[0] else x[1] for x in enumerate(data)]
    else:
        data[keys[-1]]=value
        return data

マッパー:

def map_at_path(data0, keys, f):
    data = deepcopy(data0)
    if len(keys)>1:
        if isinstance(data,dict):
            return {k:(map_at_path(v,keys[1:],f) if k==keys[0] else v) for k,v in data.items()}
        if isinstance(data,list):
            return [map_at_path(x[1],keys[1:],f) if x[0]==keys[0] else x[1] for x in enumerate(data)]
    else:
        data[keys[-1]]=f(data[keys[-1]])
        return data

0

evalPythonで関数を使用できます。

def nested_parse(nest, map_list):
    nestq = "nest['" + "']['".join(map_list) + "']"
    return eval(nestq, {'__builtins__':None}, {'nest':nest})

説明

例のクエリの場合: maplist = ["b", "v", "y"]

nestqネストされた辞書が"nest['b']['v']['y']"どこにあるかになりますnest

eval組み込み関数は、指定した文字列を実行します。ただし、eval関数の使用から生じる可能性のある脆弱性に注意することが重要です。議論はここにあります:

  1. https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
  2. https://www.journaldev.com/22504/python-eval-function

ではnested_parse()機能、私は何のことを確認してきた__builtins__グローバルは利用できませんし、利用可能な唯一のローカル変数があるnest辞書。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.