Внесение изменений в изменяемый атрибут в python
Я использую свойства для выполнения некоторого кода каждый раз, когда происходит изменение атрибута, например:
class SomeClass(object):
def __init__(self,attr):
self._attr = attr
@property
def attr(self):
return self._attr
@attr.setter
def attr(self,value):
if self._attr != value:
self._on_change()
self._attr = value
def _on_change(self):
print "Do some code here every time attr changes"
И это отлично работает:
>>> a = SomeClass(5)
>>> a.attr = 10
Do some code here every time attr changes
Но если я буду хранить измененный объект в attr
вместо этого, attr
может быть изменен напрямую, минуя setter и мой код обнаружения изменений:
class Container(object):
def __init__(self,data):
self.data = data
>>> b = SomeClass(Container(5))
>>> b.attr.data = 10
>>>
Предположим, что attr
только когда-либо будет использоваться для хранения объекта типа Container
. Есть ли элегантный способ модифицировать SomeClass
и/или Container
чтобы SomeClass
выполнял _on_change
всякий раз, когда объект Container
хранящийся в attr
был изменен? Другими словами, я хочу, чтобы мой результат был следующим:
>>> b = SomeClass(Container(5))
>>> b.attr.data = 10
Do some code here every time attr changes
Ответы
Ответ 1
Вот версия SomeClass
и Container
которая, как я думаю, имеет поведение, которое вы ищете. Идея здесь в том, что изменения в Container
_on_change()
функцию _on_change()
экземпляра SomeClass
которая связана с ней:
class Container(object):
def __init__(self, data):
self.data = data
def __setattr__(self, name, value):
if not hasattr(self, name) or getattr(self, name) != value:
self.on_change()
super(Container, self).__setattr__(name, value)
def on_change(self):
pass
class SomeClass(object):
def __init__(self, attr):
self._attr = attr
self._attr.on_change = self._on_change
@property
def attr(self):
return self._attr
@attr.setter
def attr(self,value):
if self._attr != value:
self._on_change()
self._attr = value
def _on_change(self):
print "Do some code here every time attr changes"
Пример:
>>> b = SomeClass(Container(5))
>>> b.attr.data = 10
Do some code here every time attr changes
>>> b.attr.data = 10 # on_change() not called if the value isn't changing
>>> b.attr.data2 = 'foo' # new properties being add result in an on_change() call
Do some code here every time attr changes
Обратите внимание, что единственное изменение в SomeClass
было второй строкой в __init__()
, я просто включил полный код для полноты и простого тестирования.
Ответ 2
Если вы хотите отслеживать изменения и не хотите возиться с жонглированием с on_change()
методов on_change()
в разных классах, вы можете использовать functools.partial
в порядке, показанном здесь.
Таким образом, вы можете обернуть свои данные и скрыть их полностью. Получить/изменить можно только с помощью некоторых методов, сложенных внутри этого объекта.
NB: у python нет личных свойств, и по соглашению мы все взрослые и действуем против правил. В вашем случае пользователи вашего api не должны напрямую изменять данные на контейнере (после создания).
NB: здесь для тех, кто может быть заинтересован в других путях...
Ответ 3
Вот еще одно решение. Какой-то прокси- класс. Вам не нужно изменять классы для мониторинга атрибутов в них изменения, только обернуть объект в ChangeTrigger
производного класса с ovverriden _on_change
функции:
class ChangeTrigger(object):
def __getattr__(self, name):
obj = getattr(self.instance, name)
# KEY idea for catching contained class attributes changes:
# recursively create ChangeTrigger derived class and wrap
# object in it if getting attribute is class instance/object
if hasattr(obj, '__dict__'):
return self.__class__(obj)
else:
return obj
def __setattr__(self, name, value):
if getattr(self.instance, name) != value:
self._on_change(name, value)
setattr(self.instance, name, value)
def __init__(self, obj):
object.__setattr__(self, 'instance', obj)
def _on_change(self, name, value):
raise NotImplementedError('Subclasses must implement this method')
Пример:
class MyTrigger(ChangeTrigger):
def _on_change(self, name, value):
print "New value for attr %s: %s" % (name, value)
class Container(object):
def __init__(self, data):
self.data = data
class SomeClass(object):
attr_class = 100
def __init__(self, attr):
self.attr = attr
self.attr_instance = 5
>>> a = SomeClass(5)
>>> a = MyTrigger(a)
>>>
>>> a.attr = 10
New value for attr attr: 10
>>>
>>> b = SomeClass(Container(5))
>>> b = MyTrigger(b)
>>>
>>> b.attr.data = 10
New value for attr data: 10
>>> b.attr_class = 100 # old value = new value
>>> b.attr_instance = 100
New value for attr attr_instance: 100
>>> b.attr.data = 10 # old value = new value
>>> b.attr.data = 100
New value for attr data: 100