Обновление вложенного словаря

Есть dict, например:

{'a': {'b': {'c': {'d': 1}}, 'e': 'ref:a.b.c'}}

ключ e - это путь (ключей словаря) для замены на значение из этого же словаря.

В итоге должен получиться словарь:

{'a': {'b': {'c': {'d': 1}}, 'e': {'d': 1}}}

Есть ли встроенные модули для решения такой задачи или как создать функцию (или метод), который изменяет переданный словарь или возвращает новый?


Ответы (5 шт):

Автор решения: S. Nick

Я не уверен, что правильно вас понял, но попробуйте так:

_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)
→ Ссылка
Автор решения: GrAnd

Если делать нечто более-менее универсальное, то предлагаю для определения ссылки завести отдельный класс для более наглядного хранения и лёгкого обнаружения. Тогда требуемый функционал можно реализовать как-то так:

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:])
→ Ссылка
Автор решения: slippyk
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}}}
→ Ссылка
Автор решения: SergFSM

я тоже не вполне уверен, что правильно понял ваши пожелания, но, в принципе получить нужный результат можно так:

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}}}
→ Ссылка
Автор решения: Andy Pavlov

Предложу свой вариант универсального решения. Используем две функции. Первая 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}}}}
→ Ссылка