Dostęp do zagnieżdżonych pozycji słownika za pomocą listy kluczy?

Mam złożoną strukturę słownika, do której chciałbym uzyskać dostęp poprzez listę kluczy, aby zaadresować poprawną pozycję.

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

maplist = ["a", "r"]

Lub

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

Zrobiłem następujący kod, który działa, ale jestem pewien, że jest lepszy i bardziej skuteczny sposób, aby to zrobić, jeśli ktoś ma pomysł.

# 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
Author: martineau, 2013-02-04

11 answers

Użyj reduce() aby przejść do słownika:

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

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

I ponownie użyj getFromDict, aby znaleźć lokalizację do przechowywania wartości dla setInDict():

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

Wszystko oprócz ostatniego elementu w {[7] } jest potrzebne, aby znaleźć słownik 'rodzica', aby dodać wartość do, a następnie użyć ostatniego elementu, aby ustawić wartość do prawego klawisza.

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

Zauważ, że przewodnik po stylach Pythona PEP8 przepisuje nazwy snake_case dla funkcji. Powyższe działa równie dobrze w przypadku list lub mieszanki słowniki i listy, więc nazwy powinny być get_by_path() i 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
Author: Martijn Pieters,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-29 21:12:32
  1. przyjęte rozwiązanie nie będzie działać bezpośrednio dla python3-będzie wymagało from functools import reduce.
  2. wydaje się również bardziej pythoniczne użycie pętli for. Zobacz cytat z Co nowego w Pythonie 3.0.

    Usunięto reduce(). Użyj functools.reduce(), jeśli naprawdę tego potrzebujesz; jednak w 99% przypadków wyraźna pętla for jest bardziej czytelna.

  3. następnie zaakceptowane rozwiązanie nie ustawia nieistniejących zagnieżdżonych kluczy ( zwraca KeyError) - Zobacz odpowiedź @eafit rozwiązanie

Więc dlaczego nie użyć sugerowanej metody z pytania kolergy ' ego do uzyskania wartości:

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

I kod z odpowiedzi @eafit do ustawiania wartości:

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

Oba działają prosto w Pythonie 2 i 3

 25
Author: DomTomCat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-18 15:43:29

Użycie reduce jest sprytne, ale metoda set op może mieć problemy, jeśli klucze nadrzędne nie istnieją wcześniej w zagnieżdżonym słowniku. Ponieważ jest to pierwszy tak post widziałem na ten temat w mojej wyszukiwarce google, chciałbym, aby to nieco lepiej.

Metoda set in ( Ustawianie wartości w zagnieżdżonym słowniku Pythona z listą indeksów i wartością) wydaje się bardziej odporna na brak kluczy rodzicielskich. Do skopiowania:

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

Również, może być wygodne, aby mieć metodę to przemierza drzewo kluczy i uzyskuje wszystkie bezwzględne ścieżki kluczowe, dla których utworzyłem:

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

Jednym z jego zastosowań jest konwersja zagnieżdżonego drzewa na ramkę danych pandy, używając następującego kodu (zakładając, że wszystkie liście w zagnieżdżonym słowniku mają tę samą głębokość).

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
Author: eafit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:23

Ta biblioteka może być pomocna: https://github.com/akesterson/dpath-python

Biblioteka Pythona umożliwiająca dostęp i wyszukiwanie słowników poprzez / slashed / paths ala xpath

Zasadniczo pozwala przeglądać słownik tak, jakby był system plików.

 8
Author: DMfll,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-07 16:55:43

Zamiast wykonywać hit za każdym razem, gdy chcesz wyszukać wartość, co powiesz na spłaszczenie słownika raz, a następnie po prostu poszukaj klucza jak 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}

W ten sposób możesz po prostu wyszukać przedmioty za pomocą flat_dict['b:v:y'], co da ci 1.

I zamiast przemierzać słownik przy każdym wyszukiwaniu, możesz być w stanie przyspieszyć to, spłaszczając słownik i zapisując wyjście, aby wyszukiwanie od zimnego startu oznaczało załadowanie spłaszczonego słownika i po prostu wykonując wyszukiwanie klucza / wartości bez przechodzenia.

 1
Author: OkezieE,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-01 02:07:58

A może użyjesz funkcji rekurencyjnych?

Aby uzyskać wartość:

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]

I ustawić wartość:

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
Author: xyres,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-08 23:30:50

Alternatywny sposób, jeśli nie chcesz zgłaszać błędów, jeśli jeden z kluczy jest nieobecny (aby twój główny kod mógł działać bez przerwy):

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

W tym przypadku, jeśli którykolwiek z klawiszy wejściowych nie jest obecny, nie jest zwracany, co może być użyte jako sprawdzenie w kodzie głównym do wykonania alternatywnego zadania.

 1
Author: Pulkit,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-16 01:30:53

Może sprawdzić, a następnie ustawić element dict bez dwukrotnego przetwarzania wszystkich indeksów?

Rozwiązanie:

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

Przykładowy przepływ pracy:

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
Author: And0k,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-08-22 03:40:29

Czysty styl Pythona, bez żadnego importu:

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)

Wyjście

{'foo': {'bar': 'yay'}}
 0
Author: Arount,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-16 14:00:54

Rozwiązałem to rekurencją:

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

Używając Twojego przykładu:

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
Author: Poh Zi How,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-05 13:38:11

Jeśli chcesz również pracować z dowolnymi json, w tym zagnieżdżonymi listami i dictami, i ładnie obsługiwać nieprawidłowe ścieżki wyszukiwania, oto moje rozwiązanie:

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
Author: Grant Palmer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-06 21:30:01