Отслеживать изменения в списках и словарях на python?
У меня есть класс, где экземпляры этого класса должны отслеживать изменения его атрибутов.
Пример: obj.att = 2
будет легко отслеживаться, просто переопределяя __setattr__
of obj
.
Однако возникает проблема, когда атрибут, который я хочу изменить, является самим объектом, например списком или dict.
Как я могу отслеживать такие вещи, как obj.att.append(1)
или obj.att.pop(2)
?
Я собираюсь расширить список или класс словаря, но экземпляры патчей для обезьян этих классов после инициализации obj
и obj.att
, так что obj
получает уведомление, когда такие вещи, как .append
, называется. Так или иначе, это не очень элегантно.
Другой способ, о котором я могу думать, - передать экземпляр obj
в инициализацию списка, но это сломает много существующего кода плюс, кажется, еще менее изящным, чем предыдущий метод.
Любые другие идеи/предложения? Есть ли простое решение, которое мне не хватает здесь?
Ответы
Ответ 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:
- вы ограничены несколькими типами, которые знаете и ожидаете