Sanskar Jaiswal
Sanskar Jaiswal

Reputation: 23

Update elements in the nested dictionary of varying depth via given path in python

I want to update the specific elements only with the given path dynamically it can be done by selecting my_dict['m1]['m2']['m3'] = 100 but i want dynamic approach

paths = ['m1.m2.m3','m1.m2.m4','ml2.2']

Dictionary

my_dict= {
    'm1': {
        'm2': {
            'm3': 55,
            'm4' : 75
            }
        },
    'ml2': ['a', 'b', 'c']
}

for the first path (m1.m2.m3) I want to update 55 by 100 or any other number. In the dict also the last path i.e, go to the ml2 and update the 2'nd element 'b' by 'a' or any other value

Upvotes: 1

Views: 200

Answers (2)

Gabriel De Blois
Gabriel De Blois

Reputation: 156

As you can imagine, such problem may require some sort of recursivity.

Consider the following function:

def get_nested(data, *args):
    if args and data:
        element  = args[0]
        if element:
            value = data.get(element)
            return value if len(args) == 1 else get_nested(value, *args[1:])

You can now get any depth in your dictionary by doing something like:

get_nested(dictionary, first_level_key, second_level_key, key_you_want)

By following the same principle, you can set (update) a dictionary at any depth like so:

def set_nested(data, to_set, *args):
    if args and data:
        key = args[0]
        if key:
            if len(args) == 1:
                data[key] = to_set
            else:
                data[key] = set_nested(data[key], to_set, *args[1:])
    return data

That works in the following example:

d = {
    'a': {
        'b': {
            'c': 0
        }
    }
}

set_nested(d, 1, 'a', 'b', 'c')
# return: {'a': {'b': {'c': 1}}}

These functions give you the logic behind the recursive algorithms needed to fulfill your requirements. You can then easily modify them to add your own logic. Such as the fact of using strings instead of function list parameter.

Have a nice day

Upvotes: 1

Riccardo Bucco
Riccardo Bucco

Reputation: 15364

A very clean way of achieving this is by using the built-in functools.reduce function:

from functools imnport reduce

results = [reduce(dict.get, [my_dict] + path.split('.')) for path in paths]

However, this works only if your dict contains only dicts, and the sub-dicts contain only dicts, and so on. Here is an example with:

>>> paths = ['m1.m2.m3','m1.m2.m4','ml2.2']
>>> my_dict= {
...     'm1': {
...         'm2': {
...             'm3': 55,
...             'm4' : 75
...             }
...         },
...     'ml2': {'0': 'a', '1': 'b', '2': 'c'}
... }
>>> results = [reduce(dict.get, [my_dict] + path.split('.')) for path in paths]
>>> results
[55, 75, 'c']

However, you might have lists in your data, so this approach won't work as long as you don't get rid of them. Therefore you could use a function slightly more complicated than dict.get:

from functools import reduce

results = [reduce(lambda o, k: o[int(k)] if isinstance(o, list) else o[k],
                  [my_dict] + path.split('.')) for path in paths]

Now everything works with your original data!

>>> paths = ['m1.m2.m3','m1.m2.m4','ml2.2']
>>> my_dict= {
...     'm1': {
...         'm2': {
...             'm3': 55,
...             'm4' : 75
...             }
...         },
...     'ml2': ['a', 'b', 'c']
... }
>>> results = [reduce(lambda o, k: o[int(k)] if isinstance(o, list) else o[k],
...                   [my_dict] + path.split('.')) for path in paths]
>>> results
[55, 75, 'c']

Upvotes: 0

Related Questions