Uzyskać dostęp do zagnieżdżonych elementów słownika za pośrednictwem listy kluczy?


143

Mam złożoną strukturę słownika, do której chciałbym uzyskać dostęp za pomocą listy kluczy, aby zaadresować właściwą 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"]

Stworzyłem następujący kod, który działa, ale jestem pewien, że jest lepszy i wydajniejszy 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

Odpowiedzi:


230

Użyj, reduce()aby przejść przez słownik:

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

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

i użyj ponownie, getFromDictaby znaleźć lokalizację do przechowywania wartości setInDict():

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

Wszystkie elementy oprócz ostatniego mapListsą potrzebne do znalezienia słownika „nadrzędnego”, do którego zostanie dodana wartość, a następnie użyj ostatniego elementu, aby ustawić wartość na prawy klawisz.

Próbny:

>>> 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 stylu Python PEP8 określa nazwy snake_case dla funkcji . Powyższe działa równie dobrze w przypadku list lub kombinacji słowników i list, 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

1
Na ile takie przechodzenie jest niezawodne w przypadku dowolnych zagnieżdżonych struktur? Czy będzie działać również w przypadku słowników mieszanych z listami zagnieżdżonymi? Jak zmodyfikować metodę getFromDict (), aby podawała wartość default_value i aby wartość default_value miała wartość Brak? Jestem nowicjuszem w Pythonie z wieloletnim rozwojem PHP, a przed rozwojem C.
Dmitriy Sintsov

2
Również zagnieżdżony zestaw odwzorowany powinien tworzyć nieistniejące węzły, imo: listy dla kluczy całkowitych, słowniki dla kluczy łańcuchowych.
Dmitriy Sintsov

1
@ user1353510: tak się składa, że ​​jest tutaj używana zwykła składnia indeksowania, więc będzie obsługiwać również listy wewnątrz słowników. Po prostu podaj dla nich indeksy całkowite.
Martijn Pieters

1
@ user1353510: do wartości domyślnej, użytkowania try:, except (KeyError, IndexError): return default_valuewokół aktualnej returnlinii.
Martijn Pieters

1
@Georgy: użycie dict.get()zmienia semantykę, ponieważ zwraca, Nonea nie podnosi KeyErrorbrakujące nazwy. Wszelkie kolejne nazwy wyzwalają następnie AttributeError. operatorjest biblioteką standardową, nie ma potrzeby jej tutaj omijać.
Martijn Pieters

40
  1. Zaakceptowane rozwiązanie nie będzie działać bezpośrednio dla python3 - będzie wymagało rozszerzenia from functools import reduce.
  2. Również użycie forpętli wydaje się bardziej pythonowe . 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 jawna forpętla jest bardziej czytelna.

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

Dlaczego więc nie skorzystać z sugerowanej metody z pytania kolergy, aby uzyskać wartość:

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

I kod z odpowiedzi @ eafit na ustawienie wartości:

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

Oba działają bezpośrednio w Pythonie 2 i 3


6
Wolę to rozwiązanie - ale uważaj. Jeśli się nie mylę, ponieważ słowniki Pythona nie są niezmienne getFromDict, może zniszczyć osobę dzwoniącą dataDict. Ja bym copy.deepcopy(dataDict)pierwszy. Oczywiście (jak napisano) takie zachowanie jest pożądane w drugiej funkcji.
Dylan F,

15

Korzystanie z funkcji Redukcja jest sprytne, ale metoda set OP może powodować problemy, jeśli klucze nadrzędne nie istnieją wcześniej w zagnieżdżonym słowniku. Ponieważ jest to pierwszy wpis SO, jaki widziałem na ten temat w mojej wyszukiwarce Google, chciałbym go nieco ulepszyć.

Metoda set w ( Ustawianie wartości w zagnieżdżonym słowniku języka Python z uwzględnieniem listy indeksów i wartości ) wydaje się bardziej odporna na brak kluczy rodzicielskich. Aby go skopiować:

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

Wygodne może być również posiadanie metody, która przechodzi przez drzewo kluczy i uzyskuje wszystkie bezwzględne ścieżki kluczy, 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 jej zastosowań jest konwersja zagnieżdżonego drzewa do pandy DataFrame przy użyciu następującego kodu (przy założeniu, ż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)

dlaczego arbitralnie ograniczać długość argumentu „klucze” do 2 lub więcej cali nested_set?
alancalvitti


3

A co z używaniem 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

2

Czysty styl Pythona, bez 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)

Wynik

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

2

Alternatywny sposób, jeśli nie chcesz zgłaszać błędów, jeśli nie ma jednego z kluczy (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 takim przypadku, jeśli którykolwiek z kluczy wejściowych nie jest obecny, zwracany jest None, którego można użyć jako sprawdzenia w kodzie głównym w celu wykonania alternatywnego zadania.


1

Zamiast sprawdzać wydajność za każdym razem, gdy chcesz wyszukać wartość, możesz raz spłaszczyć słownik, a następnie po prostu wyszukać klucz, taki 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']które Ci podadzą1 .

Zamiast przechodzenia przez słownik przy każdym wyszukiwaniu, możesz to przyspieszyć, spłaszczając słownik i zapisując dane wyjściowe, tak aby wyszukiwanie z zimnego startu oznaczało załadowanie spłaszczonego słownika i po prostu wykonanie wyszukiwania klucza / wartości bez przechodzenie.


1

Rozwiązałem to za pomocą rekurencji:

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

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

1

Co powiesz na sprawdzenie, a następnie ustawienie elementu 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

Bardzo późno na imprezę, ale wysłanie posta na wypadek, gdyby to mogło komuś pomóc w przyszłości. W moim przypadku następująca funkcja działała najlepiej. Działa w celu wyciągnięcia dowolnego typu danych ze słownika

dict to słownik zawierający naszą wartość

lista to lista „kroków” w kierunku naszej wartości

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

Z satysfakcją można zobaczyć te odpowiedzi w przypadku posiadania dwóch statycznych metod ustawiania i uzyskiwania zagnieżdżonych atrybutów. Te rozwiązania są o wiele lepsze niż używanie zagnieżdżonych drzew https://gist.github.com/hrldcpr/2012250

Oto moja realizacja.

Zastosowanie :

Aby ustawić zagnieżdżone wywołanie atrybutu sattr(my_dict, 1, 2, 3, 5) is equal to my_dict[1][2][3][4]=5

Aby uzyskać zagnieżdżone wywołanie atrybutu 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

Proponuję użyć python-benedictdo uzyskania dostępu do zagnieżdżonych elementów za pomocą keypath.

Zainstaluj go za pomocą pip:

pip install python-benedict

Następnie:

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

Tutaj pełna dokumentacja: https://github.com/fabiocaccamo/python-benedict


0

Jeśli chcesz również mieć możliwość pracy z dowolnym jsonem, w tym zagnieżdżonymi listami i dyktami, oraz ł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

metoda łączenia ciągów:

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

Rozszerzając podejście @DomTomCat i innych, te funkcjonalne (tj. Zwracają zmodyfikowane dane przez głębokie kopiowanie bez wpływu na dane wejściowe) setter i mapper działają dla zagnieżdżonych dicti list.

seter:

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

Możesz skorzystać z tej evalfunkcji w Pythonie.

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

Wyjaśnienie

Przykładowe zapytanie: maplist = ["b", "v", "y"]

nestqbędzie "nest['b']['v']['y']"gdzie nestjest zagnieżdżony słownik.

Funkcja evalwbudowana wykonuje podany ciąg. Należy jednak uważać na możliwe luki w zabezpieczeniach, które wynikają z używania evalfunkcji. Dyskusję można znaleźć tutaj:

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

W nested_parse()funkcji upewniłem się, że żadne __builtins__zmienne globalne nie są dostępne, a nestsłownik jest jedyną dostępną zmienną lokalną .


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.