如何跟踪Python中列表和字典的变化?

20 投票
5 回答
6317 浏览
提问于 2025-04-17 10:13

我有一个类,这个类的实例需要跟踪它的属性变化。

举个例子:当你写 obj.att = 2 的时候,这种变化很容易追踪,只需要重写 __setattr__ 方法就可以了。

但是,当我想改变的属性是一个对象,比如列表或字典时,就出现了问题。

我该如何跟踪像 obj.att.append(1)obj.att.pop(2) 这样的操作呢?

我在考虑扩展列表或字典的类,但在 objobj.att 都初始化后,对这些类的实例进行“猴子补丁”,这样当调用 .appendobj 能收到通知。不过,这样做让我觉得不太优雅。

我想到的另一种方法是把 obj 的实例传入列表的初始化,但这样会破坏很多现有的代码,而且似乎比之前的方法还要不优雅。

还有其他的想法或建议吗?我是不是漏掉了什么简单的解决方案?

5 个回答

4

与其直接修改已有的代码,不如创建一个代理类:

  • 先做一个代理类,让它继承字典、列表或集合等类型
  • 在设置属性的时候,拦截这个过程,如果值是字典、列表或集合,就把它包装成代理类
  • 在代理类的 __getattribute__ 方法中,确保调用的是被包装的类型的方法,但在调用之前要做好跟踪

优点:

  • 不需要改动原来的类

缺点:

  • 你只能使用你知道和预期的几种类型
5

你可以利用集合模块中的抽象基类,字典(dict)和列表(list)就是实现了这些基类。这让你可以使用标准库提供的接口来编写代码,只需要重写一小部分方法,比如 __getitem____setitem____delitem__insert。然后在 __getattribute__ 里面把属性包裹在一个可追踪的适配器中。

import collections

class Trackable(object):
    def __getattribute__(self, name):
        attr = object.__getattribute__(self, name)
        if isinstance(attr, collections.MutableSequence):
            attr = TrackableSequence(self, attr)
        if isinstance(attr, collections.MutableMapping):
            attr = TrackableMapping(self, attr)
        return attr

    def __setattr__(self, name, value):
        object.__setattr__(self, name, value)
        # add change tracking


class TrackableSequence(collections.MutableSequence):
    def __init__(self, tracker, trackee):
        self.tracker = tracker
        self.trackee = trackee

    # override all MutableSequence's abstract methods
    # override the the mutator abstract methods to include change tracking


class TrackableMapping(collections.MutableMapping):
    def __init__(self, tracker, trackee):
        self.tracker = tracker
        self.trackee = trackee

    # override all MutableMapping's abstract methods
    # override the the mutator abstract methods to include change tracking
13

我看到这个问题时很好奇怎么解决,于是想了一个办法。虽然这个办法没有我想象中那么简单,但可能会对你有帮助。首先,看看它的效果:

class Tracker(object):
    def __init__(self):
        self.lst = trackable_type('lst', self, list)
        self.dct = trackable_type('dct', self, dict)
        self.revisions = {'lst': [], 'dct': []}


>>> obj = Tracker()            # create an instance of Tracker
>>> obj.lst.append(1)          # make some changes to list attribute
>>> obj.lst.extend([2, 3])
>>> obj.lst.pop()
3
>>> obj.dct['a'] = 5           # make some changes to dict attribute
>>> obj.dct.update({'b': 3})
>>> del obj.dct['a']
>>> obj.revisions              # check out revision history
{'lst': [[1], [1, 2, 3], [1, 2]], 'dct': [{'a': 5}, {'a': 5, 'b': 3}, {'b': 3}]}

接下来是实现这个效果的 trackable_type() 函数:

def trackable_type(name, obj, base):
    def func_logger(func):
        def wrapped(self, *args, **kwargs):
            before = base(self)
            result = func(self, *args, **kwargs)
            after = base(self)
            if before != after:
                obj.revisions[name].append(after)
            return result
        return wrapped

    methods = (type(list.append), type(list.__setitem__))
    skip = set(['__iter__', '__len__', '__getattribute__'])
    class TrackableMeta(type):
        def __new__(cls, name, bases, dct):
            for attr in dir(base):
                if attr not in skip:
                    func = getattr(base, attr)
                    if isinstance(func, methods):
                        dct[attr] = func_logger(func)
            return type.__new__(cls, name, bases, dct)

    class TrackableObject(base):
        __metaclass__ = TrackableMeta

    return TrackableObject()

这个函数基本上是用一个元类来覆盖对象的每个方法,以便在对象发生变化时记录一些修改日志。这个方法的测试不是特别全面,我也只尝试了 listdict 这两种对象类型,但对于这两种类型来说,效果还不错。

撰写回答