14

Rather than saving a duplicate of the dictionary and comparing the old with the new, alike this:

dict = { "apple":10, "pear":20 }

if ( dict_old != dict ):
   do something
   dict_old = dict

How is it possible to detect WHEN any element of a dictionary changes?

martineau
  • 119,623
  • 25
  • 170
  • 301
crankshaft
  • 2,607
  • 4
  • 45
  • 77
  • 1
    You might want to create a subclass like [this question](http://stackoverflow.com/q/2390827/645956). – grc Oct 04 '14 at 02:49
  • 2
    Can you give more context? Like, do you want to query the database and ask it if it has changed, or do you want to know the instant is changed via some sort of signal? – SethMMorton Oct 04 '14 at 04:21
  • What is the problem you are trying to solve by implementing this? – Burhan Khalid Oct 04 '14 at 07:50

8 Answers8

18

You could subclass dict and include some custom __setitem__ behavior:

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

Example usage:

In [58]: %cpaste
Pasting code; enter '--' alone on the line to stop or use Ctrl-D.
:class MyDict(dict):
:    def __setitem__(self, item, value):
:        print "You are changing the value of %s to %s!!"%(item, value)
:        super(MyDict, self).__setitem__(item, value)
:--

In [59]: d = MyDict({"apple":10, "pear":20})

In [60]: d
Out[60]: {'apple': 10, 'pear': 20}

In [61]: d["pear"] = 15
You are changing the value of pear to 15!!

In [62]: d
Out[62]: {'apple': 10, 'pear': 15}

You would just change the print statement to involve whatever checking you need to perform when modifying.

If you are instead asking about how to check whether a particular variable name is modified, it's a much trickier problem, especially if the modification doesn't happen within the context of an object or a context manager that can specifically monitor it.

In that case, you could try to modify the dict that globals or locals points to (depending on the scope you want this to happen within) and switch it out for, e.g. an instance of something like MyDict above, except the __setitem__ you custom create could just check if the item that is being updated matches the variable name you want to check for. Then it would be like you have a background "watcher" that is keeping an eye out for changes to that variable name.

The is a very bad thing to do, though. For one, it would involve some severe mangling of locals and globals which is not usually very safe to do. But perhaps more importantly, this is much easier to achieve by creating some container class and creating the custom update / detection code there.

ely
  • 74,674
  • 34
  • 147
  • 228
  • 1
    I'd like to make a kind of dictionary which detects when you change the attributes of one of its members. For example, one could have `my_dict = MyDict({1: node1, 2: node2})`, where `node1` and `node2` are instances of the `Node` class with an attribute `d`. I would like to detect when I change the value of that attribute, e.g. by `node2.d = 0`. As I understand it, `__setitem__` only works if I replace the entire entry as in `my_dict[2] = Node(d=2)`. How could I make a change of attribute also trigger such additional code? – Kurt Peek Sep 26 '17 at 09:52
  • In this case, `my_dict` only has a reference to `node2`, it is not connected to the internals of any `node2` functions. You could add helper functions to the `MyDict` class which let you pass through value change operations that will be applied to sub-items. For example something like `my_dict.mutate_attr(2, 'd', 0)`, This might mean "get the value stored at key `2`, then call `__setattr__` to change the `d` attribute to value `0`). Then in the implementation of `MyDict.mutate_attr`, you can pre-process, log, or manipulate the request for an attribute mutation. – ely Sep 26 '17 at 11:59
  • Overall it sounds risky to me though, since it would tie your choice of doing attribute mutation to the container that happens to be holding the value (`my_dict`), and nothing would truly prevent code from mutating it outside of your mechanism apart from convention. To really implement a system like you say, you would need a set of classes that register their mutation operations with a central manager class, and otherwise have true immutability, such as by using `__new__` and inheriting from `tuple`, and then register the event of replacing one immutable version of an object with another. – ely Sep 26 '17 at 12:03
11

You could create an observer, which will monitor if the content of data has been changed.

The code below should be quite self-explanatory. It should work for nested dicts and lists.

"""Observer descriptor class allows to trigger out any arbitrary action, when the content of observed
data changes.
"""

import weakref


class Observer(object):
    """Observes attached data and trigger out given action if the content of data changes.
    Observer is a descriptor, which means, it must be declared on the class definition level.

    Example:
        >>> def action(observer, instance, value):
        ...     print 'Data has been modified: %s' % value

        >>> class MyClass(object):
        ...     important_data = Observer('init_value', callback=action)

        >>> o = MyClass()
        >>> o.important_data = 'new_value'
        Data has been modified: new_value


    Observer should work with any kind of built-in data types, but `dict` and `list` are strongly advice.

    Example:
        >>> class MyClass2(object):
        ...     important_data = Observer({}, callback=action)
        >>> o2 = MyClass2()
        >>> o2.important_data['key1'] = {'item1': 'value1', 'item2': 'value2'}
        Data has been modified: {'key1': {'item2': 'value2', 'item1': 'value1'}}
        >>> o2.important_data['key1']['item1'] = range(5)
        Data has been modified: {'key1': {'item2': 'value2', 'item1': [0, 1, 2, 3, 4]}}
        >>> o2.important_data['key1']['item1'][0] = 'first'
        Data has been modified: {'key1': {'item2': 'value2', 'item1': ['first', 1, 2, 3, 4]}}


    Here is an example of using `Observer` as a base class.

    Example:
        >>> class AdvanceDescriptor(Observer):
        ...     def action(self, instance, value):
        ...         logger = instance.get_logger()
        ...         logger.info(value)
        ...
        ...     def __init__(self, additional_data=None, **kwargs):
        ...         self.additional_data = additional_data
        ...
        ...         super(AdvanceDescriptor, self).__init__(
        ...             callback=AdvanceDescriptor.action,
        ...             init_value={},
        ...             additional_data=additional_data
        ...         )
    """

    def __init__(self, init_value=None, callback=None, **kwargs):
        """
        Args:
            init_value: initial value for data, if there is none
            callback: callback function to evoke when the content of data will change; the signature of
                the callback should be callback(observer, instance, value), where:
                    observer is an Observer object, with all additional data attached to it,
                    instance is an instance of the object, where the actual data lives,
                    value is the data itself.
            **kwargs: additional arguments needed to make inheritance possible. See the example above, to get an
                idea, how the proper inheritance should look like.
                The main challenge here comes from the fact, that class constructor is used inside the class methods,
                which is quite tricky, when you want to change the `__init__` function signature in derived classes.
        """
        self.init_value = init_value
        self.callback = callback
        self.kwargs = kwargs
        self.kwargs.update({
            'callback': callback,
        })

        self._value = None

        self._instance_to_name_mapping = {}
        self._instance = None

        self._parent_observer = None

        self._value_parent = None
        self._value_index = None

    @property
    def value(self):
        """Returns the content of attached data.
        """
        return self._value

    def _get_attr_name(self, instance):
        """To respect DRY methodology, we try to find out, what the original name of the descriptor is and
        use it as instance variable to store actual data.

        Args:
            instance: instance of the object

        Returns: (str): attribute name, where `Observer` will store the data
        """
        if instance in self._instance_to_name_mapping:
            return self._instance_to_name_mapping[instance]
        for attr_name, attr_value in instance.__class__.__dict__.iteritems():
            if attr_value is self:
                self._instance_to_name_mapping[weakref.ref(instance)] = attr_name
                return attr_name

    def __get__(self, instance, owner):
        attr_name = self._get_attr_name(instance)
        attr_value = instance.__dict__.get(attr_name, self.init_value)

        observer = self.__class__(**self.kwargs)
        observer._value = attr_value
        observer._instance = instance
        return observer

    def __set__(self, instance, value):
        attr_name = self._get_attr_name(instance)
        instance.__dict__[attr_name] = value
        self._value = value
        self._instance = instance
        self.divulge()

    def __getitem__(self, key):
        observer = self.__class__(**self.kwargs)
        observer._value = self._value[key]
        observer._parent_observer = self
        observer._value_parent = self._value
        observer._value_index = key
        return observer

    def __setitem__(self, key, value):
        self._value[key] = value
        self.divulge()

    def divulge(self):
        """Divulges that data content has been change calling callback.
        """
        # we want to evoke the very first observer with complete set of data, not the nested one
        if self._parent_observer:
            self._parent_observer.divulge()
        else:
            if self.callback:
                self.callback(self, self._instance, self._value)

    def __getattr__(self, item):
        """Mock behaviour of data attach to `Observer`. If certain behaviour mutate attached data, additional
        wrapper comes into play, evoking attached callback.
        """

        def observe(o, f):
            def wrapper(*args, **kwargs):
                result = f(*args, **kwargs)
                o.divulge()
                return result

            return wrapper

        attr = getattr(self._value, item)

        if item in (
                    ['append', 'extend', 'insert', 'remove', 'pop', 'sort', 'reverse'] + # list methods
                    ['clear', 'pop', 'update']                                           # dict methods
        ):
            return observe(self, attr)
        return attr


def action(self, instance, value):
    print '>> log >', value, '<<'


class MyClass(object):
    meta = Observer('', action)


mc1 = MyClass()
mc2 = MyClass()

mc1.meta = {
    'a1': {
        'a11': 'a11_val',
        'a22': 'a22_val',
    },
    'b1': 'val_b1',
}
mc1.meta['a1']['a11'] = ['1', '2', '4']
mc1.meta['a1']['a11'].append('5')
mc1.meta.update({'new': 'new_value'})

mc2.meta = 'test'
mc2.meta = 'test2'
mc2.meta = range(10)
mc2.meta[5] = 'test3'
mc2.meta[9] = {
    'a': 'va1',
    'b': 'va2',
    'c': 'va3',
    'd': 'va4',
    'e': 'va5',
}
mc2.meta[9]['a'] = 'val1_new'


class MyClass2(object):
    pkg = Observer('', action)


mc3 = MyClass2()
mc3.pkg = 'test_myclass2'
print mc1.meta.value
Tomasz Kurgan
  • 211
  • 2
  • 9
  • I might be doing something wrong, but for me this doesn't work. Is this solution compatible with python 3.6? When creating an Observer with an initial value, like a list, a try to access it, for example with "for el in my_object.my_oberved_list: ", fails. That's because [observer]._value is not set at initialization it seems. And when overwriting "Observer"-Type attributes, like with "my_object.my_observed_list = [1, 2, 3]", of course it replaces the Observer alltogether. – dasWesen Oct 20 '19 at 12:19
3

To go a bit further than @EMS;

Subclass dict and additionally add a sentinal attribute to keep track of changes and a method to inform you if if anything has changed.

class MyDict(dict):
    def __init__(self):
        super(MyDict, self).__init__
        self.sentinal = list()
    def __setitem__(self, item, value):
        self.sentinal.append(item)
        super(MyDict, self).__setitem__(item, value)
    def __getitem__(self, item):
        self.sentinal.remove(item)
        return super(MyDict, self).__getitem__(item)
    def update(self, iterable):
        super(MyDict, self).update(iterable)
        self.sentinal.extend(k for k, v in iterable)
    def items(self):
        self.sentinal = list()
        return super(MyDict, self).items()
    def iteritems(self):
        self.sentinal = list()
        return super(MyDict, self).iteritems()
    def item_changed(self):
        return bool(self.sentinal), self.sentinal

>>> d = MyDict()
>>> d.update(((i, i*i) for i in xrange(5)))
>>> d
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
>>> d[1] = 'g'
>>> d.item_changed()
(True, [1])
>>> z = d[1]
>>> d.item_changed()
(False, [])
>>> d[3] = 'b'
>>> d[4] = 'foo'
>>> d
{0: 0, 1: 'g', 2: 4, 3: 'b', 4: 'foo'}
>>> d.item_changed()
(True, [3, 4])
>>> d.items()
[(0, 0), (1, 'g'), (2, 4), (3, 'b'), (4, 'foo')]
>>> d.item_changed()
(False, [])
>>> d.update([(0, 'bar'), (2, 'baz')])
>>> d
{0: 'bar', 1: 'g', 2: 'baz', 3: 'b', 4: 'foo'}
>>> d.item_changed()
(True, [0, 2])
>>> list(d.iteritems())
foo
[(0, 'bar'), (1, 'g'), (2, 'baz'), (3, 'b'), (4, 'foo')]
>>> d.item_changed()
(False, [])
>>> 
wwii
  • 23,232
  • 7
  • 37
  • 77
3

The simplest solution I managed to solve my particular instance of this problem was to hash a string of the collective __repr__() of each object in the dictionary and compare hashes to see if any changes were made:

checksum = make_hash(d)

def make_hash(d):
    check = ''
    for key in d:
        check += str(d[key])
    return hash(check)

if checksum != make_hash(d):
    print('Dictionary changed')
jamsek
  • 31
  • 1
  • 4
1

My jsonfile module detects changes of (nested) JSON compatible Python objects. Just subclass JSONFileRoot to adapt change detection for your needs.

>>> import jsonfile
>>> class DoSomething(jsonfile.JSONFileRoot):
...   def on_change(self):
...     print("do something")
... 
>>> d = DoSomething({"apple": 10, "pear": 20})
>>> d.data["apple"] += 1
do something
>>> d.data
{'apple': 11, 'pear': 20}
>>> d.data["plum"] = 5
do something
>>> d.data
{'apple': 11, 'pear': 20, 'plum': 5}
SzieberthAdam
  • 3,999
  • 2
  • 23
  • 31
0

No need to subclass, if you only want to detect a change in it:

dict1 == dict2

will sort you.

hd1
  • 33,938
  • 5
  • 80
  • 91
0

This is the approach I've used with several dictionaries, loaded in from excel spreadsheets, and stored as an objects data member. The is_dirty property can be checked anytime to find out if I've altered any of the dictionaries. This approach is similar to the one suggested by @jamesek using the str() to get its string representation but we may not print out all of an object's data (for objects stored in a dictionary).

@property
def is_dirty(self):
    return self.data_hash != self.get_data_hash()

def get_data_hash(self):
    data = [
        self.dictionary1,
        self.dictionary2,
        self.dictionary3,
    ]
    m = hashlib.sha256()
    for item in data:
        m.update(
            pickle.dumps(item)
        )
    return m.digest()

def load(self):
    # do stuff to load in the data to 
    # self.dictionary1, self.dictionary2 & self.dictionary3
    # then get and store the data's current hashed value
    self.data_hash = self.get_data_hash()
Peter Shannon
  • 191
  • 1
  • 3
0

I wrote a dicta Library for this need. Dicta is a full-featured dict sub-class, that detects data changes even in nested structures. It throws a callback and/or writes the data to a .json file if the nested data structure changes.

import dicta

my_dicta = dicta.Dicta()

# the callback method
def callback():
    print("Data changed!")
    print(my_dicta)

# Bind the callback method to dicta
my_dicta(callback)

# Adding or modifying data (like pop(), append(), slice(), insert()…) will throw a callback…
my_dicta["entities"] = {}
my_dicta["entities"]["persons"] = []
my_dicta["entities"]["persons"].append({"name":"john", "age":23})
my_dicta["entities"]["persons"].append({"name":"peter", "age":13})
mextex
  • 1
  • 1