web-dev-qa-db-fra.com

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

J'ai une structure de dictionnaire complexe à laquelle je voudrais accéder via une liste de clés pour adresser le bon article.

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 plus efficace 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
106
kolergy

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éutilisez getFromDict pour trouver l'emplacement dans lequel stocker la valeur pour setInDict():

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

Il ne faut que le dernier élément de mapList pour trouver le dictionnaire 'parent' auquel ajouter la valeur, puis utilisez le dernier élément pour définir la valeur sur la clé 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 guide de style Python PEP8 prescrit les noms snake_case pour les fonctions . Ce qui précède fonctionne aussi bien pour des listes que pour un mélange de dictionnaires et de listes. Les noms doivent donc ê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
167
Martijn Pieters
  1. La solution acceptée ne fonctionnera pas directement pour python3 - elle aura besoin d'un from functools import reduce
  2. De plus, il semble plus pythonique d'utiliser une boucle for. Voir la citation de Nouveautés de Python 3.0 .

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

  3. Ensuite, la solution acceptée ne définit pas de clés imbriquées non existantes (elle retourne une 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 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

28
DomTomCat

Il est malin d’utiliser Réduire, mais la méthode set de l’OP peut poser problème si les clés parent n’existent pas dans le dictionnaire imbriqué. Etant donné que c’est le premier SO message que j’ai vu à ce sujet dans ma recherche google, j’aimerais le rendre légèrement meilleur.

La méthode set in ( Définir une valeur dans un dictionnaire python imbriqué à partir d'une liste d'index et de valeur ) 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

De plus, il peut être pratique d’avoir une méthode qui traverse l’arbre de clés et d’obtenir tous les chemins de clés 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 consiste à convertir l'arborescence imbriquée en un DataFrame pandas, à l'aide du 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)
12
eafit

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

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

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

8
DMfll

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)

Sortie

{'foo': {'bar': 'yay'}}
1
Arount

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

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
And0k

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

Au lieu de prendre un coup de performance chaque fois que vous souhaitez rechercher une valeur, que diriez-vous d’aplatir le dictionnaire une fois puis recherchez simplement la clé telle que 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'] qui vous donnera 1.

Et au lieu de parcourir le dictionnaire à chaque recherche, vous pourrez peut-être accélérer le processus 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 traversal.

1
OkezieE

Une autre solution si vous ne voulez pas générer d’erreurs si l’une des clés est absente (pour que votre code principal puisse fonctionner 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 de saisie n'est pas présente, Aucune est renvoyée. Elle peut être utilisée comme vérification de votre code principal pour effectuer une autre tâche.

1
Pulkit

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
lucas

Si vous souhaitez également pouvoir travailler avec des JSON arbitraires, y compris des listes imbriquées et des dict, 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
Grant Palmer

Très tard pour 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 de "pas" 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
0
Jack Casey

En élargissant l'approche de @DomTomCat et des autres, ces objets fonctionnels (c'est-à-dire, renvoient des données modifiées via deepcopy sans affecter l'entrée), setter et mapper fonctionnent pour niché dict et 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
alancalvitti

Résolu ceci avec la 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
Poh Zi How

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 d’utiliser des arbres imbriqués https://Gist.github.com/hrldcpr/2012250

Voici ma mise en œuvre.

Usage:

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

Pour obtenir un attribut imbriqué, appelez 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:
        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 such key is not found or the value is primitive supply an empty dict
        if d.get(attr) is None or isinstance(d.get(attr), dict):
            d[attr] = {}
        d = d[attr]
    d[attrs[-2]] = attrs[-1]
0
nehemiah