Внесение изменений в изменяемый атрибут в 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