Доступ к вложенным словарным элементам через список ключей?

У меня есть сложная структура словаря, к которой я хотел бы получить доступ через список ключей, чтобы обратиться к правильному элементу.

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

maplist = ["a", "r"]

или

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

Я сделал следующий код, который работает, но я уверен, что есть лучший и эффективный способ сделать это, если у кого-то есть идея.

# 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

Ответ 1

Используйте метод reduce() для перемещения словаря:

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

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

и повторно использовать getFromDict чтобы найти местоположение для хранения значения для setInDict():

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

Все, кроме последнего элемента в mapList, необходимы, чтобы найти "родительский" словарь для добавления значения, а затем использовать последний элемент, чтобы установить значение вправо.

Демо-версия:

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

Обратите внимание, что руководство по стилю Python PEP8 предписывает именам snake_case для функций. Вышеупомянутое работает одинаково хорошо для списков или сочетания словарей и списков, поэтому имена должны быть действительно get_by_path() и 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

Ответ 2

  1. Принятое решение не будет работать непосредственно для python3 - ему потребуется from functools import reduce.
  2. Также кажется, что более pythonic использует цикл for. См. Цитату из Whats New в Python 3.0.

    Удалено reduce(). Используйте functools.reduce() если вам это действительно нужно; Тем не менее, 99 процентов времени явного for цикла является более удобным для чтения.

  3. Затем принятое решение не устанавливает несуществующие вложенные ключи (он возвращает KeyError) - см. Ответ @eafit для решения

Так почему бы не использовать предложенный метод из вопроса kolergy для получения значения:

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

И код из @eafit отвечает за установку значения:

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

Оба работают прямо в python 2 и 3

Ответ 3

Использование сокращения является умным, но метод установки OP может иметь проблемы, если родительские ключи не существуют во вложенном словаре. Поскольку это первая публикация SO, которую я видел для этого объекта в моем поиске в Google, я хотел бы сделать это немного лучше.

Метод set в (Установка значения в вложенном словаре python с учетом списка индексов и значения) кажется более надежным для отсутствующих родительских ключей. Чтобы скопировать его:

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

Кроме того, может быть удобно иметь метод, который обходит дерево ключей и получает все пути абсолютного ключа, для которых я создал:

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

Одно из них - преобразовать вложенное дерево в pandas DataFrame, используя следующий код (предполагая, что все листы в вложенном словаре имеют одинаковую глубину).

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)

Ответ 4

Эта библиотека может быть полезна: https://github.com/akesterson/dpath-python

Библиотека python для доступа и поиска словарей через /slashed/paths ala xpath

В основном это позволяет вам глотать словарь, как если бы он был файловая система.

Ответ 5

Как насчет использования рекурсивных функций?

Чтобы получить значение:

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]

И для установки значения:

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

Ответ 6

Вместо того, чтобы каждый раз, когда вы хотите найти значение, каждый раз, когда вы хотите получить значение производительности, как насчет выравнивания словаря, просто найдите ключ, например 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}

Таким образом, вы можете просто искать элементы, используя flat_dict['b:v:y'], который даст вам 1.

И вместо того, чтобы перемещаться по словарю при каждом поиске, вы можете ускорить это, сглаживая словарь и сохраняя вывод, чтобы поиск с холодного старта означал загрузку сплющенного словаря и просто выполнение ключа/значения поиск без обхода.

Ответ 7

Чистый стиль Python без импорта:

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)

Выход

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

Ответ 8

Альтернативный способ, если вы не хотите поднимать ошибки, если один из ключей отсутствует (чтобы ваш основной код мог работать без перерыва):

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

В этом случае, если какой-либо из входных клавиш отсутствует, возвращается None, который может использоваться как проверка вашего основного кода для выполнения альтернативной задачи.

Ответ 9

Как насчет проверки, а затем установить элемент dict без обработки всех индексов дважды?

Решение:

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

Пример рабочего процесса:

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

Тестовое задание

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

Ответ 10

Очень поздно на вечеринку, но размещение в случае, если это может помочь кому-то в будущем. Для моего случая использования следующая функция работала лучше всего. Работает, чтобы извлечь любой тип данных из словаря

dict словарь, содержащий наше значение

list это список "шагов" к нашей ценности

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

Ответ 11

Решено это с рекурсией:

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

Используя ваш пример:

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

Ответ 12

Если вы также хотите работать с произвольным json, включая вложенные списки и dicts, и красиво обрабатывать недопустимые пути поиска, здесь мое решение:

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

Ответ 13

Приятно видеть эти ответы, имея два статических метода для установки и получения вложенных атрибутов. Эти решения намного лучше, чем использование вложенных деревьев https://gist.github.com/hrldcpr/2012250

Здесь моя реализация.

Использование:

Чтобы установить вложенный атрибут, вызовите sattr(my_dict, 1, 2, 3, 5) is equal to my_dict[1][2][3][4]=5

Чтобы получить вложенный атрибут, вызовите 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]

Ответ 14

метод объединения строк:

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

Ответ 15

Расширяя подход @DomTomCat и других, эти функциональные (то есть возвращающие измененные данные с помощью глубокой копии без влияния на ввод) установщики и преобразователи работают для вложенных dict и list.

сеттер:

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

картограф:

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