Accéder aux éléments de dictionnaire imbriqués via une liste de clés?


143

J'ai une structure de dictionnaire complexe à laquelle 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 créé le code suivant qui fonctionne, mais je suis sûr qu'il existe un moyen meilleur et plus efficace de le 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

Réponses:


230

Utilisez 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 getFromDictpour trouver l'emplacement pour stocker la valeur pour setInDict():

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

Tous les éléments sauf le dernier mapListsont nécessaires pour trouver le dictionnaire «parent» auquel ajouter la valeur, puis utilisez le dernier élément pour définir la valeur sur la bonne clé.

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 guide de style Python PEP8 prescrit des noms snake_case pour les fonctions . Ce qui précède fonctionne aussi bien pour les listes que pour un mélange de dictionnaires et de listes, donc 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

1
Dans quelle mesure ce parcours est-il fiable pour les structures imbriquées arbitraires? Cela fonctionnera-t-il également pour les dictionnaires mixtes avec des listes imbriquées? Comment modifier getFromDict () pour fournir default_value et pour que default_value par défaut soit None? Je suis novice en Python avec de nombreuses années de développement PHP et avant le développement C.
Dmitriy Sintsov

2
Un ensemble mappé imbriqué devrait également créer des nœuds non existants, imo: listes pour les clés entières, dictionnaires pour les clés de chaîne.
Dmitriy Sintsov

1
@ user1353510: en l'occurrence, la syntaxe d'indexation régulière est utilisée ici, donc elle supportera également les listes dans les dictionnaires. Passez simplement des index entiers pour ceux-ci.
Martijn Pieters

1
@ user1353510: pour une valeur par défaut, utilisez try:, except (KeyError, IndexError): return default_valueautour de la returnligne courante .
Martijn Pieters

1
@Georgy: l'utilisation dict.get()change la sémantique, car elle renvoie Noneplutôt que relance KeyErrorpour les noms manquants. Tous les noms suivants déclenchent alors un AttributeError. operatorest une bibliothèque standard, il n'est pas nécessaire de l'éviter ici.
Martijn Pieters

40
  1. La solution acceptée ne fonctionnera pas directement pour python3 - elle aura besoin d'un from functools import reduce.
  2. Il semble également plus pythonique d'utiliser une forboucle. Voir la citation de Quoi de neuf dans Python 3.0 .

    Supprimé reduce(). À utiliser functools.reduce()si vous en avez vraiment besoin; cependant, 99% du temps, une forboucle explicite est plus lisible.

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

Alors pourquoi ne pas utiliser la méthode suggérée de 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 définir une valeur:

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

Les deux fonctionnent directement en python 2 et 3


6
Je préfère cette solution - mais soyez prudent. Si je ne me trompe pas, puisque les dictionnaires Python ne sont pas immuables, cela getFromDictpeut potentiellement détruire l'appelant dataDict. Je voudrais d' copy.deepcopy(dataDict)abord. Bien sûr, (comme écrit) ce comportement est souhaité dans la deuxième fonction.
Dylan F

15

L'utilisation de la réduction est intelligente, mais la méthode set de l'OP peut avoir des problèmes si les clés parentes n'existent pas dans le dictionnaire imbriqué. Comme il s'agit du premier article SO que j'ai vu pour ce sujet dans ma recherche Google, j'aimerais l'améliorer légèrement.

La méthode set dans ( Définition d'une valeur dans un dictionnaire python imbriqué à partir d'une liste d'indices et de valeurs ) semble plus robuste aux clés parentales manquantes. 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 pratique d'avoir une méthode qui traverse l'arborescence des clés et obtenir tous les chemins de clé absolus, pour lesquels 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()], [])

Une utilisation de celui-ci est de convertir l'arborescence imbriquée en un DataFrame pandas, en utilisant le code suivant (en supposant que toutes les feuilles du dictionnaire imbriqué 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)

pourquoi limiter arbitrairement la longueur de l'argument «clés» à 2 ou plus nested_set?
alancalvitti

10

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

Une bibliothèque python pour accéder et rechercher des dictionnaires via / slashed / chemins ala xpath

Fondamentalement, il vous permet de parcourir un dictionnaire comme s'il s'agissait d'un système de fichiers.


3

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 définir 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

2

Style pur 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)

Production

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

2

Une autre manière si vous ne souhaitez pas déclencher d'erreurs si l'une des clés est absente (pour 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 clés d'entrée n'est pas présente, None est renvoyé, ce qui peut être utilisé comme vérification de votre code principal pour effectuer une tâche alternative.


1

Au lieu de prendre une performance à chaque fois que vous souhaitez rechercher une valeur, que diriez-vous d'aplatir le dictionnaire une fois, puis de rechercher simplement 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 rechercher des éléments en utilisant flat_dict['b:v:y']ce qui vous donnera1 .

Et au lieu de parcourir le dictionnaire à chaque recherche, vous pourrez peut-être accélérer cela en aplatissant le dictionnaire et en enregistrant la sortie de sorte qu'une recherche à partir d'un démarrage à froid signifierait charger le dictionnaire aplati et simplement effectuer une recherche clé / valeur sans traversée.


1

Résolu cela avec la récursivité:

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

1

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 workflow:

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

Tester

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

Très tard à la fête, mais poster au cas où cela pourrait aider quelqu'un à l'avenir. Pour mon cas d'utilisation, la fonction suivante a fonctionné le mieux. Fonctionne pour extraire n'importe quel type de données du dictionnaire

dict est le dictionnaire contenant notre valeur

list est une liste des "étapes" vers notre valeur

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

Il est satisfaisant de voir ces réponses pour avoir deux méthodes statiques pour définir et obtenir des attributs imbriqués. Ces solutions sont bien meilleures que l'utilisation d'arbres imbriqués https://gist.github.com/hrldcpr/2012250

Voici ma mise en œuvre.

Usage :

Pour définir un appel d'attribut imbriqué sattr(my_dict, 1, 2, 3, 5) is equal to my_dict[1][2][3][4]=5

Pour obtenir un appel d'attribut imbriqué 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

Je vous suggère d'utiliser python-benedictpour accéder aux éléments imbriqués en utilisant keypath.

Installez-le en utilisant pip:

pip install python-benedict

Ensuite:

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'])

Voici la documentation complète: https://github.com/fabiocaccamo/python-benedict


0

Si vous souhaitez également pouvoir travailler avec des json arbitraires, y compris des listes imbriquées et des dictionnaires, et gérer correctement les chemins de recherche non valides, 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

une méthode pour concaténer des chaînes:

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

En étendant l'approche de @DomTomCat et d'autres, ces setter et mappeur fonctionnels (c'est-à-dire retournent des données modifiées via deepcopy sans affecter l'entrée) fonctionnent pour imbriqués dictet list.

setter:

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

mappeur:

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

Vous pouvez utiliser la evalfonction en python.

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

Explication

Pour votre exemple de requête: maplist = ["b", "v", "y"]

nestqsera "nest['b']['v']['y']"nest trouve le dictionnaire imbriqué.

La evalfonction intégrée exécute la chaîne donnée. Cependant, il est important de faire attention aux éventuelles vulnérabilités résultant de l'utilisation de la evalfonction. La discussion peut être trouvée ici:

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

Dans la nested_parse()fonction, je me suis assuré qu'aucun __builtins__global n'est disponible et que seule la variable locale disponible est le nestdictionnaire.


En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.