Accéder à des articles de dictionnaires imbriqués via une liste de clés?

j'ai une structure de dictionnaire complexe que je voudrais accéder via une liste de clés pour adresser l'élément correct.

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

maplist = ["a", "r"]

ou

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

j'ai fait le code suivant qui fonctionne, mais je suis sûr qu'il y est une meilleure et plus efficace pour ce faire, si quelqu'un a une idée.

# 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
99
demandé sur martineau 2013-02-04 22:04:35

11 réponses

Utiliser reduce() pour parcourir le dictionnaire:

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

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

et réutiliser getFromDict pour trouver l'endroit où stocker la valeur de setInDict() :

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

tout sauf le dernier élément de mapList est nécessaire pour trouver le dictionnaire 'parent' pour ajouter la valeur, puis utiliser le dernier élément pour définir la valeur à la touche de droite.

Démo:

>>> 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}}

notez que le Python PEP8 guide de style prescrit des noms snake_case pour les fonctions . Ce qui précède fonctionne également bien pour les listes ou un mélange de dictionnaires et de listes, de sorte que les noms devraient vraiment être get_by_path() et 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
151
répondu Martijn Pieters 2018-07-29 21:12:32
  1. la solution acceptée ne fonctionnera pas directement pour python3 - elle aura besoin d'un from functools import reduce .
  2. il semble aussi plus pythonique d'utiliser une boucle for . Voir la citation de Quoi de neuf en Python 3.0 .

    supprimé reduce() . Utilisez functools.reduce() si vous en avez vraiment besoin; cependant, 99% du temps une boucle for explicite est plus lisible.

  3. ensuite, la solution acceptée ne définit pas les clés imbriquées non existantes (elle renvoie un KeyError )-voir la réponse de @eafit pour une solution

alors pourquoi ne pas utiliser la méthode suggérée dans la question de kolergy pour obtenir une valeur:

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

et le code de la réponse de @eafit pour fixer une valeur:

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

les Deux à travailler tout en python 2 et 3

25
répondu DomTomCat 2018-03-18 15:43:29

en utilisant reduce est intelligent, mais la méthode de jeu de L'OP peut avoir des problèmes si les clés de parent ne pré-existent pas dans le dictionnaire imbriqué. Comme il s'agit du premier poste SO que j'ai vu pour ce sujet dans ma recherche google, je voudrais le rendre un peu mieux.

la méthode set dans ( définir une valeur dans un dictionnaire Python imbriqué donné une liste d'indices et de valeur ) semble plus robuste à manquer des clés parentales. Pour le copier:

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

en outre, il peut être commode d'avoir une méthode qui traverse l'arbre de la clé et obtenir tous les chemins de la clé absolue, pour lequel j'ai créé:

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()], [])

un usage de celui-ci est de convertir l'arbre emboîté en une base de données pandas, en utilisant le code suivant (en supposant que toutes les feuilles dans le dictionnaire emboîté ont la même profondeur).

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)
9
répondu eafit 2017-05-23 12:26:23

cette bibliothèque peut être utile: https://github.com/akesterson/dpath-python

une bibliothèque python pour accéder et rechercher des dictionnaires via /réduit/les chemins de l'ala xpath

Fondamentalement, il vous permet de glob sur un dictionnaire comme si c'était un système de fichiers.

8
répondu DMfll 2015-07-07 16:55:43

au lieu de prendre un coup de performance à chaque fois que vous voulez chercher une valeur, que diriez-vous d'aplatir le dictionnaire une fois puis simplement chercher la clé comme 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}

de cette façon, vous pouvez simplement chercher des articles en utilisant flat_dict['b:v:y'] qui vous donnera 1 .

et au lieu de traverser le dictionnaire sur chaque recherche, vous pouvez être en mesure d'accélérer ce en aplatissant le dictionnaire et en économisant la sortie de sorte qu'une recherche dès le départ à froid, cela signifierait charger le dictionnaire aplati et simplement effectuer une recherche clé/valeur sans traversée.

1
répondu OkezieE 2017-03-01 02:07:58

Que Diriez-vous d'utiliser des fonctions récursives?

pour obtenir une valeur:

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]

et pour fixer une valeur:

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
1
répondu xyres 2017-12-08 23:30:50

une alternative si vous ne voulez pas soulever d'erreurs si l'une des touches est absente (de sorte que votre code principal puisse s'exécuter sans interruption):

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

Dans ce cas, si l'une des touches d'entrée n'est pas présent, rien n'est renvoyé, qui peut être utilisé comme un contrôle dans votre code principal pour effectuer une autre tâche.

1
répondu Pulkit 2018-03-16 01:30:53

Que Diriez-vous de vérifier puis de définir l'élément dict sans traiter tous les index deux fois?

Solution:

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

exemple de flux de travail:

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()

Test

>>> 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
répondu And0k 2018-08-22 03:40:29

pur style Python, sans aucune importation:

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)

sortie

{'foo': {'bar': 'yay'}}
0
répondu Arount 2018-02-16 14:00:54

résolu par récursion:

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

en utilisant votre exemple:

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
0
répondu Poh Zi How 2018-03-05 13:38:11

si vous voulez aussi la possibilité de travailler avec JSON arbitraire, y compris les listes imbriquées et les dicts, et bien gérer les chemins de recherche invalides, voici ma solution:

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
répondu Grant Palmer 2018-03-06 21:30:01