如何检测字典中的任何元素是否发生变化?

时间:2014-10-04 02:40:19

标签: python dictionary

不是保存字典的重复,而是将旧字词与新字典进行比较,类似于:

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

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

如何检测词典的任何元素何时发生变化?

6 个答案:

答案 0 :(得分:8)

您可以继承dict并包含一些自定义__setitem__行为:

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 [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}

您只需更改print语句即可包含修改时需要执行的任何检查。

如果您正在询问如何检查特定变量名是否被修改,则这是一个非常棘手的问题,特别是如果修改不是在对象或可以专门监视它的上下文管理器的上下文中发生的。

在这种情况下,您可以尝试修改dictglobals指向的locals(取决于您希望在此范围内发生的范围)并且例如,将其切换出来上面MyDict之类的实例,除了您自定义创建的__setitem__之外,可以检查正在更新的项是否与您要检查的变量名匹配。然后,就像你有一个背景“观察者”,正在密切关注该变量名称的变化。

但是,这是一件非常糟糕的事情。首先,它会涉及localsglobals的严重错误,这通常不是很安全。但也许更重要的是,通过创建一些容器类并在那里创建自定义更新/检测代码,这更容易实现。

答案 1 :(得分:8)

您可以创建一个观察者,它将监视数据内容是否已更改。

下面的代码应该是不言自明的。它应该适用于嵌套的dicts和列表。

"""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

答案 2 :(得分:3)

比@EMS更进一步;

子类dict并另外添加sentinal属性以跟踪更改,并提供一种方法来通知您是否有任何更改。

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, [])
>>> 

答案 3 :(得分:1)

我设法解决此问题的最简单方法是对字典中每个对象的集合__repr__()的字符串进行哈希处理,并比较散列以查看是否进行了任何更改:

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')

答案 4 :(得分:0)

如果您只想检测其中的更改,则无需子类:

dict1 == dict2

会对你进行排序。

答案 5 :(得分:0)

我的jsonfile模块检测到(嵌套的)JSON兼容Python对象的更改。只需将JSONFileRoot子类化即可使更改检测适应您的需求。

>>> 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}