Могу ли я получить доступ к вложенному dict со списком ключей?
Я хотел бы получить доступ к словарю программно. Я знаю, как это сделать с рекурсивной функцией, но есть ли более простой способ?
example = {'a': {'b': 'c'},
'1': {'2': {'3': {'4': '5'}}}}
keys = ('a', 'b')
example[keys] = 'new'
# Now it should be
# example = {'a': {'b': 'new'},
# '1': {'2': {'3': {'4': '5'}}}}
keys = ('1', '2', '3', '4')
example[keys] = 'foo'
# Now it should be
# example = {'a': {'b': 'new'},
# '1': {'2': {'3': {'4': 'foo'}}}}
keys = ('1', '2')
example[keys] = 'bar'
# Now it should be
# example = {'a': {'b': 'new'},
# '1': {'2': 'bar'}}
Ответы
Ответ 1
Это решение создает еще один словарь с такими же ключами, а затем обновляет существующий словарь:
#!/usr/bin/env python
from six.moves import reduce
def update2(input_dictionary, new_value, loc):
"""
Update a dictionary by defining the keys.
Parameters
----------
input_dictionary : dict
new_value : object
loc : iterable
Location
Returns
-------
new_dict : dict
Examples
--------
>>> example = {'a': {'b': 'c'}, '1': {'2': {'3': {'4': '5'}}}}
>>> update2(example, 'new', ('a', 'b'))
{'a': {'b': 'new'}, '1': {'2': {'3': {'4': '5'}}}}
>>> update2(example, 'foo', ('1', '2', '3', '4'))
{'a': {'b': 'new'}, '1': {'2': {'3': {'4': 'foo'}}}}
>>> update2(example, 'bar', ('1', '2'))
{'a': {'b': 'new'}, '1': {'2': 'bar'}}
"""
new_dict = reduce(lambda x, y: {y: x}, reversed(loc), new_value)
input_dictionary.update(new_dict)
return input_dictionary
if __name__ == '__main__':
import doctest
doctest.testmod()
используйте строку, список или кортеж для ключей доступа
Ответ 2
То, что вы, похоже, хотите сделать, это определить свой собственный словарь, поддерживающий такой тип индексирования. Мы можем достичь довольно аккуратного синтаксиса, используя тот факт, что когда вы выполняете d[1, 2, 3]
, Python фактически передает кортеж (1, 2, 3)
в __getitem__
.
class NestedDict:
def __init__(self, *args, **kwargs):
self.dict = dict(*args, **kwargs)
def __getitem__(self, keys):
# Allows getting top-level branch when a single key was provided
if not isinstance(keys, tuple):
keys = (keys,)
branch = self.dict
for key in keys:
branch = branch[key]
# If we return a branch, and not a leaf value, we wrap it into a NestedDict
return NestedDict(branch) if isinstance(branch, dict) else branch
def __setitem__(self, keys, value):
# Allows setting top-level item when a single key was provided
if not isinstance(keys, tuple):
keys = (keys,)
branch = self.dict
for key in keys[:-1]:
if not key in branch:
branch[key] = {}
branch = branch[key]
branch[keys[-1]] = value
Вот примеры использования
# Getting an item
my_dict = NestedDict({'a': {'b': 1}})
my_dict['a', 'b'] # 1
# Setting an item
my_dict = NestedDict()
my_dict[1, 2, 3] = 4
my_dict.dict # {1: {2: {3: 4}}}
# You can even get a branch
my_dict[1] # NestedDict({2: {3: 4}})
my_dict[1][2, 3] # 4
Затем вы можете сделать реализацию NestedDict
богаче, также определяя __iter__
, __len__
и __contains__
.
Кроме того, это может быть легко интегрировано в ваш код, так как любой уже существующий словарь можно превратить в вложенный, выполнив NestedDict(your_dict)
.
Ответ 3
После адаптации этого ответа (для косметики по существу) я получил:
from functools import reduce
import operator
def nested_get(dictionary, *keys):
return reduce(operator.getitem, keys, dictionary)
def nested_set(dictionary, value, *keys):
nested_get(dictionary, *keys[:-1])[keys[-1]] = value
example = {
'a': {'b': 'c'},
'1': {
'2': {
'3': {'4': '5'}
}
}
}
nested_set(example, "foo", "1", "2", "3")
print(example)
keys = ["1", "2"]
nested_set(example, "yay", *keys)
print(example)
Вывод:
{'a': {'b': 'c'}, '1': {'2': {'3': 'foo'}}}
{'a': {'b': 'c'}, '1': {'2': 'yay'}}
То же самое (использовать тот факт, что переменные передаются по ссылке), но на этот раз адаптировав один из моих ответов:
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" } } }
nested_set(example, "yay", "foo", "bar")
print(example)
Выход
{'foo': {'bar': 'yay'}}
Это не требует каких-либо причудливых импортов, поэтому мне нравится больше...
Выберите свой вкус
Ответ 4
Вы можете использовать меньшую рекурсивную функцию со пониманием словаря:
import functools
def all_examples(f):
def wrapper():
def update_dict(d, path, target):
return {a:target if path[-1] == a else update_dict(b, path, target) if isinstance(b, dict) else b for a, b in d.items()}
current_d = {'a': {'b': 'c'},'1': {'2': {'3': {'4': '5'}}}}
final_ds = []
for i in f():
current_d = update_dict(current_d, *i)
final_ds.append(current_d)
return final_ds
return wrapper
@all_examples
def input_data():
return [[('a', 'b'), 'new'], [('1', '2', '3', '4'), 'foo'], [('1', '2'), 'bar']]
for i in input_data():
print(i)
Вывод:
{'a': {'b': 'new'}, '1': {'2': {'3': {'4': '5'}}}}
{'a': {'b': 'new'}, '1': {'2': {'3': {'4': 'foo'}}}}
{'a': {'b': 'new'}, '1': {'2': 'bar'}}
Ответ 5
Сделайте обход методом вашего словаря. Вы можете сделать это легко, подклассифицируя dict
.
Алгоритм перемещения - это @MartijnPeters (вверху).
import operator
class ndict(dict):
def get_traverse(self, mapList):
return reduce(operator.getitem, mapList, self)
def set_traverse(self, mapList, value):
self.get_traverse(mapList[:-1])[mapList[-1]] = value
d = ndict({'a': {'b': 'c'}, '1': {'2': {'3': {'4': '5'}}}})
d.get_traverse(['a', 'b']) # 'c'
d.set_traverse(['a', 'b'], 4) # {'a': {'b': 4}, '1': {'2': {'3': {'4': '5'}}}}
Ответ 6
Я хотел добавить ссылку на dict-digger, который является модулем с открытым исходным кодом, который мне иногда нравится использовать, который предоставляет эту функциональность из коробки. (Я никоим образом не связан с этим проектом)
Ответ 7
Я объединил ответ jpp и Olivier Melançon, чтобы создать класс NestedDict. Я предпочитаю обращаться к диктовке, как вы обычно получаете диктовку, но затем со списком в качестве аргумента
import operator
from functools import reduce
class NestedDict(dict):
def __getitem__(self, item):
if isinstance(item, list):
return self.get_traverse(item)
return super(NestedDict, self).__getitem__(item)
def __setitem__(self, key, value):
if isinstance(key, list):
for _key in key[:-1]:
if not _key in self:
self[_key] = {}
self = self[_key]
self[key[-1]] = value
return self
return super(NestedDict, self).__setitem__(key, value)
def get_traverse(self, _list):
return reduce(operator.getitem, _list, self)
nested_dict = NestedDict({'aggs': {'aggs': {'field_I_want': 'value_I_want'}, 'None': None}})
path = ['aggs', 'aggs', 'field_I_want']
nested_dict[path] # 'value_I_want'
nested_dict[path] = 'changed'
nested_dict[path] # 'changed'
редактировать
Для всех, кто заинтересован. Я расширил класс функцией автоматического поиска путей. (Использование в doctest)
import operator
from functools import reduce
from copy import deepcopy
class NestedDict(dict):
"""
Dictionary that can use a list to get a value
:example:
>>> nested_dict = NestedDict({'aggs': {'aggs': {'field_I_want': 'value_I_want'}, 'None': None}})
>>> path = ['aggs', 'aggs', 'field_I_want']
>>> nested_dict[path]
'value_I_want'
>>> nested_dict[path] = 'changed'
>>> nested_dict[path]
'changed'
"""
def __getitem__(self, item):
if isinstance(item, list):
return self.get_traverse(item)
return super(NestedDict, self).__getitem__(item)
def __setitem__(self, key, value):
if isinstance(key, list):
for _key in key[:-1]:
if _key not in self:
self[_key] = {}
self = self[_key]
self[key[-1]] = value
return self
return super(NestedDict, self).__setitem__(key, value)
def get_traverse(self, _list):
return reduce(operator.getitem, _list, self)
_paths = []
_path = []
def find(self, key, _dict=None, _main_loop=True):
""" Find a list of paths to a key
:param key: str, the key you want to find
:param _dict: used for recursive searching
:return: list with paths
:example:
>>> nested_dict = NestedDict({'aggs': {'aggs': {'field_I_want': 'value_I_want'}, 'None': None}})
>>> paths = nested_dict.find('field_I_want')
>>> paths
[['aggs', 'aggs', 'field_I_want']]
>>> nested_dict[paths[0]] = 'changed'
>>> nested_dict[paths[0]]
'changed'
"""
if _main_loop:
self._path, self._paths = [], []
_dict = self
for _key in _dict.keys():
self._path.append(_key)
if _key == key:
self._paths.append(deepcopy(self._path))
if isinstance(_dict[_key], dict):
self.find(key, _dict[_key], _main_loop=False)
self._path.pop()
if _main_loop:
return self._paths