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


151

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

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

Ответы:


244

Используйте 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

И для завершения, функция для удаления ключа:

def del_by_path(root, items):
    """Delete a key-value in a nested object in root by item sequence."""
    del get_by_path(root, items[:-1])[items[-1]]

3
Также вложенный сопоставленный набор должен создавать несуществующие узлы, imo: списки для целочисленных ключей, словари для строковых ключей.
Дмитрий Синцов

@ user1353510: разные сценарии использования требуют разного поведения. Код здесь не создает посредников, нет.
Мартин Питерс

1
@ user1353510: для значения по умолчанию используйте try:, except (KeyError, IndexError): return default_valueвокруг текущей returnстроки.
Мартейн Питерс

@ user1353510: См. список вложенных словарей в Python для другого варианта использования; используя, dict.setdefault()а не dict.__getitem__.
Мартейн Питерс

42

Кажется более питоническим использовать forцикл. См. Цитату из статьи «Что нового в Python 3.0» .

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

def nested_get(dic, keys):    
    for key in keys:
        dic = dic[key]
    return dic

Обратите внимание, что принятое решение не устанавливает несуществующие вложенные ключи (оно возникает KeyError). При использовании следующего подхода вместо этого будут созданы несуществующие узлы:

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

Код работает как в Python 2, так и в 3.


6
Я предпочитаю это решение, но будьте осторожны. Если я не ошибаюсь, поскольку словари Python не являются неизменяемыми, getFromDictони могут уничтожить вызывающий dataDict. Я бы copy.deepcopy(dataDict)первым. Конечно (как написано) такое поведение желательно во второй функции.
Dylan F

На самом деле это не имеет ничего общего с изменчивостью - это просто вопрос переназначения имени переменной dataDict новой переменной (подкаталоги)
naught101

@DylanF Не могли бы вы объяснить, как это может уничтожить ввод? Похоже, что мне просто повторно привязывают имя локальной переменной.
wim

@wim Я думаю, что я имел в виду, если вы извлекаете изменяемый объект и начинаете его изменять, вы также меняете объект в исходном словаре. Оглядываясь назад, я не знаю, действительно ли это удивительное поведение. Просто нужно иметь в виду.
Дилан Ф,

1
@DylanF Хорошо, теперь понятно. Но это getFromDictсамо по себе не разрушает вызывающего dataDict? Это связано с изменением возвращаемого значения, которое было сделано вне функции. Пользователь всегда может сделать копию, если он не хочет этого, но нет возможности отменить копию, сделанную внутри функции, поэтому не копировать более гибко.
Вим

16

Использование reduce - это разумно, но метод set 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()], [])

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

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)

зачем произвольно ограничивать длину аргумента «ключи» до 2 или более дюймов nested_set?
alancalvitti

10

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

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

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


2

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

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

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

2

Решил это с помощью рекурсии:

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

очень хорошо, я добавил дополнительное условие if для обработки недостающих ключей: def get(d,l, default_val=None): if l[0] not in d: return default_val elif len(l)==1: return d[l[0]] else: return get(d[l[0]],l[1:])
Мэтт

1

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

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


1

Чистый стиль 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'}}

1

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

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, которое можно использовать в качестве проверки в вашем основном коде для выполнения альтернативной задачи.


1

Приятно видеть эти ответы на наличие двух статических методов для установки и получения вложенных атрибутов. Эти решения намного лучше, чем использование вложенных деревьев 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]

0

Если вам также нужна возможность работать с произвольным 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

0

Как насчет проверки, а затем установки элемента 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()

0

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

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


-1

метод конкатенации строк:

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

-1

Расширяя подход @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

-1

Вы можете использовать эту evalфункцию в Python.

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

Объяснение

Для вашего примера запроса: maplist = ["b", "v", "y"]

nestqбудет "nest['b']['v']['y']"где nestвложенный словарь.

evalФункция выполняет встроенный в данную строку. Однако важно быть осторожным в отношении возможных уязвимостей, возникающих в результате использования evalфункции. Обсуждение можно найти здесь:

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

В этой nested_parse()функции я убедился, что __builtins__глобальные переменные недоступны, а доступная только локальная переменная является nestсловарем.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.