Доступ до вкладених елементів словника за допомогою списку клавіш?


143

У мене є складна структура словника, до якої я хотів би отримати доступ через список клавіш для адреси правильного пункту.

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

Відповіді:


230

Використовуйте 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
Наскільки таке пересування надійне для довільних вкладених структур? Чи буде це працювати і для змішаних словників із вкладеними списками? Як я можу змінити getFromDict (), щоб надати default_value і мати типовий default_value як None? Я початківець у Python, який мав багаторічну розробку PHP та до розвитку C.
Дмитро Сінцов

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

1
@ user1353510: як це буває, тут використовується синтаксис регулярної індексації, тому він також підтримуватиме списки всередині словників. Просто передайте цілі індекси для них.
Martijn Pieters

1
@ User1353510: для значень по замовчуванню, використання try:, except (KeyError, IndexError): return default_valueнавколо поточної returnрядки.
Martijn Pieters

1
@Georgy: використання dict.get()змін семантики, оскільки це повертається, Noneа не збільшується KeyErrorдля відсутніх імен. Будь-які наступні імена потім запускають an AttributeError. operatorце стандартна бібліотека, тут не потрібно уникати цього.
Martijn Pieters

40
  1. Прийняте рішення не працюватиме безпосередньо для python3 - воно знадобиться from functools import reduce.
  2. Крім того, більш пітонічним видається використання forпетлі. Дивіться цитату з новинок у Python 3.0 .

    Вилучено reduce(). Використовуйте, functools.reduce()якщо вам це справді потрібно; проте 99 відсотків часу явного forциклу є більш читабельним.

  3. Далі, прийняте рішення не встановлює неіснуючі вкладені ключі (воно повертає a KeyError) - див. Відповідь @ eafit для рішення

То чому б не скористатися запропонованим методом з питання про колергію для отримання значення:

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

Обидва працюють прямо в пітоні 2 і 3


6
Я вважаю за краще це рішення - але будьте обережні. Якщо я не помиляюсь, оскільки словники Python не є незмінними getFromDict, це може знищити викликає dataDict. Я б copy.deepcopy(dataDict)першим. Звичайно, (як написано) така поведінка бажана у другій функції.
Ділан Ф

15

Використовувати скорочення розумно, але у встановленого методу ОП можуть виникнути проблеми, якщо батьківські ключі не існують у вкладеному словнику. Оскільки це перша публікація SO, яку я побачив для цієї теми під час пошуку в Google, я хотів би зробити її трохи кращою.

Метод set у ( Встановлення значення в словнику вкладеного пітона з переліком індексів і значення ) здається більш надійним для відсутніх батьківських ключів. Щоб скопіювати його:

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

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

Бібліотека пітона для доступу та пошуку словників через / slashhed / paths ala xpath

В основному це дозволяє вам переглядати словник так, ніби це файлова система.


3

Як щодо використання рекурсивних функцій?

Щоб отримати значення:

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

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

2

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

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

У цьому випадку, якщо жодної клавіші введення немає, не повертається жодна, що може бути використане як перевірка вашого основного коду для виконання альтернативного завдання.


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

Вирішили це за допомогою рекурсії:

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

1

Як щодо перевірки, а потім встановити елемент 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()

1

Дуже пізно на вечірку, але розміщення у випадку, якщо це може допомогти комусь у майбутньому. У моєму випадку використання наступна функція спрацювала найкраще. Працює з виведення будь-якого типу даних із словника

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

Ці задоволення бачити ці відповіді щодо наявності двох статичних методів встановлення та отримання вкладених атрибутів. Ці рішення набагато кращі, ніж використання вкладених дерев 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]

1

Я пропоную вам використовувати python-benedictдля доступу до вкладених елементів за допомогою keypath.

Встановіть його за допомогою pip:

pip install python-benedict

Тоді:

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

Ось повна документація: https://github.com/fabiocaccamo/python-benedict


0

Якщо ви також хочете можливість працювати з довільним 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

0

метод об'єднання рядків:

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

Розширюючи підхід @DomTomCat та інших, ці функціональні (тобто повертають змінені дані за допомогою deepcopy, не впливаючи на вхід), сеттер і картограф працює для вкладених 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

0

Ви можете скористатися 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.