Обновление вложенного словаря
Есть dict, например:
{'a': {'b': {'c': {'d': 1}}, 'e': 'ref:a.b.c'}}
ключ e - это путь (ключей словаря) для замены на значение из этого же словаря.
В итоге должен получиться словарь:
{'a': {'b': {'c': {'d': 1}}, 'e': {'d': 1}}}
Есть ли встроенные модули для решения такой задачи или как создать функцию (или метод), который изменяет переданный словарь или возвращает новый?
Ответы (5 шт):
Я не уверен, что правильно вас понял, но попробуйте так:
_dict = {
'a': {
'b': {
'c': {'d': 1}
},
'e': 'ref:a.b.c'
}
}
print(_dict)
ref = _dict['a']['e'][4:].split('.')
d = _dict
for k in ref:
d = d[k]
new_ref = d
_dict['a']['e'] = new_ref
print(_dict)
Если делать нечто более-менее универсальное, то предлагаю для определения ссылки завести отдельный класс для более наглядного хранения и лёгкого обнаружения. Тогда требуемый функционал можно реализовать как-то так:
class Ref:
def __init__(self, path):
if isinstance(path, list):
self.path = '.'.join(path)
else:
self.path = path
def get_path(self):
return self.path
def dict_get_by_path(d:dict, key:str, default=None):
def get_one_level(d, key_list, level=0):
if level < len(key_list)-1:
return get_one_level(d[key_list[level]], key_list, level+1)
elif level == len(key_list)-1:
return d[key_list[level]]
raise IndexError
try:
return get_one_level(d, key.split('.'))
except KeyError:
return default
def replace_refs(d, sub_d=None):
if sub_d is None: sub_d = d
for k,v in sub_d.items():
if isinstance(v, dict):
replace_refs(d, v)
elif isinstance(v, Ref):
sub_d[k] = dict_get_by_path(d, v.get_path())
d = {'a': {'b': {'c': {'d': 1}}, 'e': Ref('a.b.c')}}
replace_refs(d)
print(d)
Где dict_get_by_path() - функция, которая достает значение из словаря по пути вида "a.b.c". А replace_refs() рекурсивно проходит по всем элементам словаря и при обнаружении ссылок Ref заменяет их на значения по ссылкам.
Если очень хочется без отдельного класса, то вместо
elif isinstance(v, Ref):
sub_d[k] = dict_get_by_path(d, v.get_path())
можно написать
elif isinstance(v, str) and v.startswith('ref:'):
sub_d[k] = dict_get_by_path(d, v[4:])
def changer(origin, link={}):
if not link:
link = origin
for key, value in link.items():
if isinstance(value, dict):
changer(origin, value)
continue
if (isinstance(value, str)) and ('ref' in value):
tmp = origin
for way in value.split(':')[1].split('.'):
tmp = tmp[way]
link[key] = tmp
return origin
print(changer({'a': {'b': {'c': {'d': 1}}, 'e': 'ref:a.b.c'}}))
# {'a': {'b': {'c': {'d': 1}}, 'e': {'d': 1}}}
я тоже не вполне уверен, что правильно понял ваши пожелания, но, в принципе получить нужный результат можно так:
dct = {'a': {'b': {'c': {'d': 1}},
'e': 'ref:a.b.c'}}
path = dct['a']['e'].strip('ref:').split('.')
dct['a']['e'] = (f:= lambda x,y: x if not y else f(x[y.pop(0)],y))(dct,path)
print(dct) # {'a': {'b': {'c': {'d': 1}}, 'e': {'d': 1}}}
UPD
если суть вопроса в том, чтобы сначала найти все ссылки, а потом сделать соответствующие замены, то вот такой метод вроде дает нужный результат:
class Ref:
def __call__(self, _dict):
self._dict = _dict
self.search(self._dict)
return self._dict
def search(self, d): # ищем ссылки
for k in d:
if type(d[k])==dict: self.search(d[k])
if type(d[k])==str and d[k][:4]=='ref:':
d[k] = self.sub(d[k][4:].split('.'), self._dict)
def sub(self, to_sub, dct): # заменяем ссылки на значения, полученные по ссылкам
if not to_sub: return dct
return self.sub(to_sub, dct[to_sub.pop(0)])
ввод:
dict_to_sub = {'a': {'b': {'c': {'d': 1},
'h': {'i': 2}},
'e': 'ref:a.b.c',
'f': 'ref:a.b.h'}}
sub_ref = Ref()
sub_ref(dict_to_sub)
вывод:
{'a': {'b': {'c': {'d': 1}, 'h': {'i': 2}}, 'e': {'d': 1}, 'f': {'i': 2}}}
Предложу свой вариант универсального решения. Используем две функции. Первая find() для поиска значения по указанному пути, организованная через reduce и operator.getitem. Вторая dict_refresh проходит рекурсивно словарь в поисках ссылки. Как только находит ее, то через find() подставляем значение по ссылке. Если такой ссылки нет, то будет замена на None.
from functools import reduce
import operator
def find(d, path):
try:
return reduce(operator.getitem, path.split('.'), d)
except KeyError:
return None
def dict_refresh(d, d_init=None):
for k in d:
if isinstance(d[k], dict):
dict_refresh(d[k], d_init or d)
if isinstance(d[k], str) and d[k].startswith('ref:'):
d[k] = find(d_init, d[k].replace('ref:', ''))
d = {'a': {'b': {'c': {'d': 1}}, 'e': {'f': 'ref:a.b.c'}}}
dict_refresh(d)
print(d)
# {'a': {'b': {'c': {'d': 1}}, 'e': {'d': 1}}}
Для комментария GrAnd:
d = {'a': {'b': {'c': {'d': 1}}, 'e': {'f': 'ref:a.b.c'}}}
dict_refresh(d)
print(d)
# {'a': {'b': {'c': {'d': 1}}, 'e': {'f': {'d': 1}}}}