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



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



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
788   11  

11 ответов:

использовать 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
  1. принятое решение не будет работать непосредственно для python3 - ему понадобится from functools import reduce.
  2. также, кажется, более подходящие для Python, чтобы использовать for петли. Смотрите цитату из что нового в 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

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

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)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Comments

    Ничего не найдено.