How to detect if any element in a dictionary changes?
Solution 1:
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.
Solution 2:
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