【问题标题】:Track changes to lists and dictionaries in python?跟踪python中列表和字典的更改?
【发布时间】:2012-01-13 23:14:32
【问题描述】:

我有一个类,该类的实例需要跟踪其属性的更改。

例如:obj.att = 2 是可以通过简单地覆盖 __setattr__obj 来轻松跟踪的东西。

但是,当我要更改的属性是对象本身时,例如列表或字典,就会出现问题。

我如何能够跟踪 obj.att.append(1)obj.att.pop(2) 之类的内容?

我正在考虑扩展列表或字典类,但是一旦objobj.att 都被初始化,那么猴子修补这些类的实例,以便obj.append 之类的事情发生时得到通知叫。不知何故,这感觉不是很优雅。

我能想到的另一种方法是将obj 的实例传递给列表初始化,但这会破坏许多现有代码,而且它似乎比以前的方法更不优雅。

还有其他想法/建议吗?我在这里缺少一个简单的解决方案吗?

【问题讨论】:

  • 如何让对象成为实习生并只允许通过 getter 和 setter 类从外部访问?
  • 您可以序列化对象并比较序列化的版本。
  • 我同意乔尼的观点。在这里考虑得墨忒耳定律。或者,禁止直接访问 attr,但提供创建深层副本的 getter 和 setter。

标签: python


【解决方案1】:

当我看到这个问题时,我很好奇这是如何实现的,这是我想出的解决方案。不像我希望的那么简单,但它可能很有用。首先,这是行为:

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 之外,我还没有尝试过任何其他对象类型,但它似乎对这些对象有效。

【讨论】:

  • 是的,基本上,我试图找到一种简单的方法来做到这一点,这样您就不必创建单独的 listdict 子类并找出您需要的方法打补丁,显然我想出的并不是那么简单:)
【解决方案2】:

您可以创建一个代理类,而不是猴子修补:

  • 创建一个从 dict/list/set 继承的代理类
  • 拦截属性设置,如果值为dict/list/set,则将其包装到代理类中
  • 在代理类 __getattribute__ 中,确保在包装类型上调用该方法,但在此之前注意跟踪。

专业版:

  • 无类更改

缺点:

  • 您仅限于一些您知道和期望的类型

【讨论】:

    【解决方案3】:

    您可以利用集合模块中的抽象基类,由 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
    

    【讨论】:

      【解决方案4】:

      您还可以包装您想要跟踪的字典或列表方法,并确保您在包装器中执行您想要的操作。这是一个字典示例:

      from functools import wraps
      
      def _setChanged(func):
          @wraps(func)
          def wrappedFunc(self, *args, **kwargs):
              self.changed = True
              return func(self, *args, **kwargs)
          return wrappedFunc
      
      def _trackObjectMethods(calssToTrack):
          for methodName in dir(calssToTrack):
              if methodName in calssToTrack._destructive:
                  setattr(calssToTrack, methodName, _setChanged(getattr(calssToTrack, methodName)))
      
      class Dictionary(dict):
          _destructive = ('__delitem__', '__setitem__', 'clear', 'pop', 'popitem', 'setdefault', 'update')
      
          def __init__(self, *args, **kwargs):
              self.changed = False
              super().__init__(*args, **kwargs)
      
      _trackObjectMethods(Dictionary)
      
      d = Dictionary()
      print(d.changed)
      d["1"] = 'test'
      print(d.changed)
      d.changed = False
      print(d.changed)
      d["12"] = 'test2'
      print(d.changed)
      

      如您所见,如果字典的任何项目发生更改,我添加到自定义 Dictionary 对象的自定义变量将设置为 True。这样我就可以判断自上次我将 changed 变量设置为 False 后对象是否发生了变化。

      【讨论】:

        【解决方案5】:

        我的jsonfile 模块检测到(嵌套的)JSON 兼容 Python 对象的变化。只需继承 JSONFileRoot 即可根据您的需要调整变更检测。

        >>> import jsonfile
        >>> class Notify(jsonfile.JSONFileRoot):
        ...   def on_change(self):
        ...     print(f'notify: {self.data}')
        ... 
        >>> test = Notify()
        >>> test.data = 1
        notify: 1
        >>> test.data = [1,2,3]
        notify: [1, 2, 3]
        >>> test.data[0] = 12
        notify: [12, 2, 3]
        >>> test.data[1] = {"a":"b"}
        notify: [12, {'a': 'b'}, 3]
        >>> test.data[1]["a"] = 20
        notify: [12, {'a': 20}, 3]
        

        请注意,它以 e-satis 建议的代理类方式进行,不支持集合。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2010-12-24
          • 1970-01-01
          • 1970-01-01
          • 2015-12-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-12-07
          相关资源
          最近更新 更多