Подтвердить что ты не робот

Отслеживать изменения в списках и словарях на python?

У меня есть класс, где экземпляры этого класса должны отслеживать изменения его атрибутов.

Пример: obj.att = 2 будет легко отслеживаться, просто переопределяя __setattr__ of obj.

Однако возникает проблема, когда атрибут, который я хочу изменить, является самим объектом, например списком или dict.

Как я могу отслеживать такие вещи, как obj.att.append(1) или obj.att.pop(2)?

Я собираюсь расширить список или класс словаря, но экземпляры патчей для обезьян этих классов после инициализации obj и obj.att, так что obj получает уведомление, когда такие вещи, как .append, называется. Так или иначе, это не очень элегантно.

Другой способ, о котором я могу думать, - передать экземпляр obj в инициализацию списка, но это сломает много существующего кода плюс, кажется, еще менее изящным, чем предыдущий метод.

Любые другие идеи/предложения? Есть ли простое решение, которое мне не хватает здесь?

4b9b3361

Ответ 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()

В основном используется метакласс для переопределения каждого метода объекта для добавления некоторого журнала изменений, если объект изменяется. Это не очень тщательно проверено, и я не пробовал никаких других типов объектов помимо list и dict, но, похоже, для них это хорошо.

Ответ 2

Вы можете воспользоваться абстрактными базовыми классами в модуле коллекций, который реализует 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 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 abstract methods
    # override the the mutator abstract methods to include change tracking

Ответ 3

Вместо исправления обезьян вы можете создать прокси-класс:

  • Сделать прокси-класс, который наследует от dict/list/set any
  • Установить параметр атрибута перехвата, а если значение - это dict/list/set, вставьте его в прокси-класс
  • В прокси-классе __getattribute__ убедитесь, что метод вызывается для завернутого типа, но позаботьтесь о его отслеживании.

Pro:

  • без изменения класса

Con:

  • вы ограничены несколькими типами, которые знаете и ожидаете