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
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
- la solution acceptée ne fonctionnera pas directement pour python3 - elle aura besoin d'un
from functools import reduce
. - il semble aussi plus pythonique d'utiliser une boucle
for
. Voir la citation de Quoi de neuf en Python 3.0 .supprimé
reduce()
. Utilisezfunctools.reduce()
si vous en avez vraiment besoin; cependant, 99% du temps une bouclefor
explicite est plus lisible. - 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
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)
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.
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.
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
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.
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()
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'}}
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
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