Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect changes to a nested dictionary with Python

I've seen a few similar questions on SO regarding detecting changes to a dictionary and calling a function when the dictionary changes, such as:

  • How to trigger function on value change?
  • python detect if any element in a dictionary changes

These examples use variations of the Observer pattern or overloading __setitem__, but all these examples don't detect changes on nested dictionary values.

For example, if I have:

my_dict = {'a': {'b': 1}}
my_dict['a']['b'] = 2

The assignment of 2 to the element ['a']['b'] will not be detected.

I'm wondering if there is an elegant way of detecting changes not only to the base elements of a dictionary but all the child elements of a nested dictionary as well.

like image 735
Willem van Ketwich Avatar asked Dec 21 '18 09:12

Willem van Ketwich


People also ask

How do you access nested dictionary items in Python?

To access element of a nested dictionary, we use indexing [] syntax in Python.

How an element can be detected from a dictionary in Python?

Accessing Elements from Dictionary Keys can be used either inside square brackets [] or with the get() method. If we use the square brackets [] , KeyError is raised in case a key is not found in the dictionary. On the other hand, the get() method returns None if the key is not found.

How do you check if a value is in a nested dictionary Python?

Use get() and Key to Check if Value Exists in a Dictionary Dictionaries in Python have a built-in function key() , which returns the value of the given key. At the same time, it would return None if it doesn't exist.

How do you change a nested dictionary in Python?

Adding or updating nested dictionary items is easy. Just refer to the item by its key and assign a value. If the key is already present in the dictionary, its value is replaced by the new one. If the key is new, it is added to the dictionary with its value.


3 Answers

Building on the answer given in here, just do the following:

class MyDict(dict):
    def __setitem__(self, item, value):
        print("You are changing the value of {} to {}!!".format(item, value))
        super(MyDict, self).__setitem__(item, value)

and then:

my_dict = MyDict({'a': MyDict({'b': 1})})

my_dict['a']['b'] = 2

You are changing the value of b to 2!!

my_dict['a'] = 5

You are changing the value of a to 5!!

If you want to avoid manual calls to MyDict at each nesting level, one way of doing it, is to fully overload the dict class. For example:

class MyDict(dict):
    def __init__(self,initialDict):
        for k,v in initialDict.items():
          if isinstance(v,dict):
            initialDict[k] = MyDict(v)
        super().__init__(initialDict)

    def __setitem__(self, item, value):
        if isinstance(value,dict):
          _value = MyDict(value)
        else:
          _value = value
        print("You are changing the value of {} to {}!!".format(item, _value))
        super().__setitem__(item, _value)

You can then do the following:

# Simple initialization using a normal dict synthax
my_dict = MyDict({'a': {'b': 1}})

# update example
my_dict['c'] = {'d':{'e':4}}

You are changing the value of c to {'d': {'e': 4}}!!

my_dict['a']['b'] = 2
my_dict['c']['d']['e'] = 6

You are changing the value of b to 2!!

You are changing the value of e to 6!!

like image 163
ma3oun Avatar answered Oct 21 '22 18:10

ma3oun


Complete solution borrowing from the this link(the second one given by OP)

class MyDict(dict):
    def __setitem__(self, item, value):
        print("You are changing the value of {key} to {value}!!".format(key=item, value=value))
        super(MyDict, self).__setitem__(item, convert_to_MyDict_nested(value))

def convert_to_MyDict_nested(d):
    if not(isinstance(d, dict)):
        return d
    for k, v in d.items():
        if isinstance(v, dict):
            d[k] = convert_to_MyDict_nested(v)
    return MyDict(d)

So that if

d = {'a': {'b': 1}}

then,

d = convert_to_MyDict_nested(d)
d['a']['b'] = 2  # prints You are changing the value of b to 2!!
d['a']= 5  # prints You are changing the value of a to 5!!

Also, edited according to comment by OP. So,

d["c"] = {"e" : 7}  # prints You are changing the value of c to {'e': 7}!!
d["c"]["e"] = 9  # prints You are changing the value of e to 9!!
like image 26
Deepak Saini Avatar answered Oct 21 '22 18:10

Deepak Saini


You can write an adapter class that automatically wraps values in itself, like this (untested, but I think it illustrates the point):

class DictChangeListenerAdapter:
    def __init__(self, original, listener):
        self._original = original
        self._listener = listener

    def __getitem__(self, key):
        value = self._original.__getitem__(key)
        if isinstance(value, dict):
            return DictChangeListenerAdapter(self._original[key], self._listener)
        else:
            return value

    def __setitem__(self, key, value):
        self._original.__setitem__(key, value)
        self._listener(self, key, value)

Note that this is going to cause access to the wrapped items to be much more expensive, use with care.

like image 1
Lie Ryan Avatar answered Oct 21 '22 16:10

Lie Ryan