Accedere agli elementi del dizionario nidificati tramite un elenco di chiavi?


143

Ho una struttura di dizionario complessa a cui vorrei accedere tramite un elenco di chiavi per indirizzare l'elemento corretto.

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

maplist = ["a", "r"]

o

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

Ho creato il seguente codice che funziona, ma sono sicuro che esiste un modo migliore e più efficiente per farlo se qualcuno ha un'idea.

# 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

Risposte:


230

Utilizzare reduce()per attraversare il dizionario:

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

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

e riutilizzare getFromDictper trovare la posizione in cui memorizzare il valore per setInDict():

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

Sono necessari tutti gli elementi tranne l'ultimo mapListper trovare il dizionario 'parent' a cui aggiungere il valore, quindi utilizzare l'ultimo elemento per impostare il valore sulla chiave giusta.

demo:

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

Si noti che la guida di stile Python PEP8 prescrive i nomi snake_case per le funzioni . Quanto sopra funziona ugualmente bene per elenchi o un mix di dizionari ed elenchi, quindi i nomi dovrebbero essere get_by_path()e 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
Quanto è affidabile tale spostamento per strutture nidificate arbitrarie? Funzionerà anche con dizionari misti con elenchi nidificati? Come posso modificare getFromDict () per fornire default_value e avere default_value predefinito come None? Sono alle prime armi in Python con molti anni di sviluppo in PHP e prima dello sviluppo in C.
Dmitriy Sintsov,

2
Anche il set mappato nidificato dovrebbe creare nodi inesistenti, imo: elenchi per chiavi intere, dizionari per chiavi stringa.
Dmitriy Sintsov l'

1
@ user1353510: in questo caso, qui viene utilizzata la sintassi di indicizzazione regolare, quindi supporterà anche gli elenchi all'interno dei dizionari. Basta passare in indici interi per quelli.
Martijn Pieters

1
@ user1353510: per un valore predefinito, utilizzare try:, except (KeyError, IndexError): return default_valueattorno alla returnriga corrente .
Martijn Pieters

1
@Georgy: usando dict.get()cambia la semantica, in quanto restituisce Noneanziché rilanciare KeyErrorper nomi mancanti. Eventuali nomi successivi attivano quindi un AttributeError. operatorè una libreria standard, non è necessario evitarlo qui.
Martijn Pieters

40
  1. La soluzione accettata non funzionerà direttamente per python3 - avrà bisogno di un from functools import reduce.
  2. Inoltre sembra più pitonico usare un forloop. Vedi la citazione da Novità di Python 3.0 .

    Rimosso reduce(). Usa functools.reduce()se ne hai davvero bisogno; tuttavia, il 99 percento delle volte in cui un forciclo esplicito è più leggibile.

  3. Successivamente, la soluzione accettata non imposta chiavi nidificate inesistenti (restituisce a KeyError) - vedere la risposta di @ eafit per una soluzione

Quindi perché non usare il metodo suggerito dalla domanda di Kolergy per ottenere un valore:

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

E il codice dalla risposta di @ eafit per impostare un valore:

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

Entrambi funzionano direttamente in Python 2 e 3


6
Preferisco questa soluzione, ma fai attenzione. Se non sbaglio, dal momento che i dizionari Python non sono immutabili getFromDictha il potenziale per distruggere il chiamante dataDict. Vorrei copy.deepcopy(dataDict)prima io . Naturalmente (come scritto) questo comportamento è desiderato nella seconda funzione.
Dylan F,

15

L'uso di riduci è intelligente, ma il metodo set del PO può avere problemi se le chiavi padre non preesistono nel dizionario nidificato. Dato che questo è il primo post SO che ho visto per questo argomento nella mia ricerca su Google, vorrei migliorarlo leggermente.

Il metodo set in ( Impostazione di un valore in un dizionario python nidificato con un elenco di indici e valore ) sembra più robusto rispetto alle chiavi parentali mancanti. Per copiarlo:

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

Inoltre, può essere conveniente disporre di un metodo che attraversa l'albero delle chiavi e ottenere tutti i percorsi delle chiavi assoluti, per i quali ho creato:

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 suo uso è convertire l'albero nidificato in un DataFrame panda, usando il seguente codice (supponendo che tutte le foglie nel dizionario nidificato abbiano la stessa profondità).

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)

perché limitare arbitrariamente la lunghezza dell'argomento "chiavi" a 2 o più in nested_set?
alancalvitti,

10

Questa libreria può essere utile: https://github.com/akesterson/dpath-python

Una libreria Python per l'accesso e la ricerca di dizionari tramite / slashed / percorsi ala xpath

Fondamentalmente ti permette di glob su un dizionario come se fosse un filesystem.


3

Che ne dici di usare le funzioni ricorsive?

Per ottenere un valore:

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]

E per impostare un valore:

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

Puro stile Python, senza alcuna importazione:

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)

Produzione

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

2

Un modo alternativo se non si desidera generare errori se una delle chiavi è assente (in modo che il codice principale possa essere eseguito senza interruzioni):

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

In questo caso, se una delle chiavi di input non è presente, viene restituito None, che può essere utilizzato come controllo nel codice principale per eseguire un'attività alternativa.


1

Invece di subire un colpo di performance ogni volta che vuoi cercare un valore, che ne dici di appiattire il dizionario una volta e poi semplicemente cercare la chiave come 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}

In questo modo puoi semplicemente cercare gli oggetti usando ciò flat_dict['b:v:y']che ti darà 1.

E invece di attraversare il dizionario su ogni ricerca, potresti essere in grado di accelerarlo appiattendo il dizionario e salvando l'output in modo che una ricerca da avvio a freddo significherebbe caricare il dizionario appiattito ed eseguire semplicemente una ricerca chiave / valore senza attraversamento.


1

Risolto questo con ricorsione:

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

Usando il tuo esempio:

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

Che ne dici di controllare e quindi impostare l'elemento dict senza elaborare due volte tutti gli indici?

Soluzione:

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

Esempio di flusso di lavoro:

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

Molto tardi alla festa, ma pubblicare nel caso ciò possa aiutare qualcuno in futuro. Nel mio caso d'uso, la seguente funzione ha funzionato al meglio. Funziona per estrarre qualsiasi tipo di dati dal dizionario

dict è il dizionario che contiene il nostro valore

list è un elenco di "passi" verso il nostro valore

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

È soddisfacente vedere queste risposte per avere due metodi statici per impostare e ottenere attributi nidificati. Queste soluzioni sono molto meglio dell'utilizzo di alberi nidificati https://gist.github.com/hrldcpr/2012250

Ecco la mia implementazione.

Utilizzo :

Per impostare la chiamata di attributo nidificata sattr(my_dict, 1, 2, 3, 5) is equal to my_dict[1][2][3][4]=5

Per ottenere una chiamata di attributo nidificata 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

Ti suggerisco di usare python-benedictper accedere agli oggetti nidificati usando keypath.

Installalo usando pip:

pip install python-benedict

Poi:

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

Ecco la documentazione completa: https://github.com/fabiocaccamo/python-benedict


0

Se vuoi anche la possibilità di lavorare con json arbitrari inclusi elenchi e dadi nidificati e gestire correttamente percorsi di ricerca non validi, ecco la mia soluzione:

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

un metodo per concatenare stringhe:

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

Estendendo @DomTomCat e l'approccio altrui, questi setter e mapper funzionali (ovvero restituiscono i dati modificati tramite deepcopy senza influire sull'input) funzionano per nidificati dicte 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

Mapper:

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

Puoi usare la evalfunzione in Python.

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

Spiegazione

Per la tua query di esempio: maplist = ["b", "v", "y"]

nestqsarà "nest['b']['v']['y']"dove si nesttrova il dizionario nidificato.

La evalfunzione integrata esegue la stringa specificata. Tuttavia, è importante fare attenzione alle possibili vulnerabilità derivanti dall'uso della evalfunzione. La discussione può essere trovata qui:

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

Nella nested_parse()funzione, mi sono assicurato che non __builtins__siano disponibili globali e che solo la variabile locale disponibile sia il nestdizionario.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.