18

I am using properties to execute some code every time there is a change to an attribute, like this:

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"

And this works great:

>>> a = SomeClass(5)
>>> a.attr = 10
Do some code here every time attr changes

But if I store a mutable object in attr instead, attr can be modified directly, bypassing the setter and my change-detection code:

class Container(object):
    def __init__(self,data):
        self.data = data

>>> b = SomeClass(Container(5))
>>> b.attr.data = 10
>>>

Let's assume that attr is only ever going to be used to store an object of type Container. Is there an elegant way to modify SomeClass and/or Container to make SomeClass execute _on_change whenever the Container object stored in attr is modified? In other words, I want my output to be:

>>> b = SomeClass(Container(5))
>>> b.attr.data = 10
Do some code here every time attr changes
Emma
  • 1,287
  • 2
  • 17
  • 22
  • Can't you just do that in your `@attr.setter` method? – martineau Nov 08 '13 at 22:48
  • 1
    @martineau -- No, `attr` isn't being set here. `attr.data` is what is being set. – mgilson Nov 08 '13 at 22:54
  • By the way, enthought Traits directly supports this kind of notify-on-change behavior, including notifying parent objects when a child is modified. It is overkill for this application but if you are regularly using complex notification patterns like this it might be useful. – Evan Nov 08 '13 at 23:35

3 Answers3

7

Here is another solution. Some kind of proxy class. You dont need to modify any classes to monitor attributes changes in them, only wrap object in ChangeTrigger derived class with ovverriden _on_change function:

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')

Example:

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
ndpu
  • 22,225
  • 6
  • 54
  • 69
  • 3
    This solution is much better than the accepted solution, I'd suggest using this instead. – Raven Aug 21 '19 at 10:46
  • One point though, shouldn't you switch the __getattr__ in the trigger class to __getattribute__ instead? – Raven Aug 21 '19 at 11:05
  • 1
    @raven `__getattr__` invoked only with attributes that doesn't exist in trigger class itself - supposed that this attributes is in contained instance. This is example, maybe there is situations in which you should use `__getattribute__` instead... – ndpu Sep 02 '19 at 10:53
4

Here is a version of SomeClass and Container that I think has the behavior you are looking for. The idea here being that modifications to Container will call the _on_change() function of the SomeClass instance that is associated with it:

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"

Example:

>>> 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

Note that the only change to SomeClass was the second line in __init__(), I just included the full code for completeness and easy testing.

Andrew Clark
  • 202,379
  • 35
  • 273
  • 306
  • 1
    Great as long as one can modify the `Container` class, which unfortunately rules out it being used directly with any mutable built-in container classes like `dict`, `list`, etc. – martineau Nov 09 '13 at 02:06
2

If you want to track changes and don't want to mess with juggling with on_change() methods in different classes you could use functools.partial in the way shown starting here.

This way you can wrap your data and hide it totally. Get/change will be possible only via some methods melded inside that object.

NB: python has no private properties and on convention that we all are grownups and act against rules. In your case users of your api shouldn't change data on container (after creation) directly.

NB: here for those who may be interested in other ways...

twil
  • 6,032
  • 1
  • 30
  • 28
  • You bring up a good point with the grownups thing. In this case though, I do expect my users to read from `data`, I just don't want them to write to it after it has been added to a `SomeClass` object. When an attribute is private it's easy to indicate that with an underscore. How would you indicate that an attribute should be treated as read-only? – Emma Nov 09 '13 at 00:02
  • Python convention here is [PEP 8](http://www.python.org/dev/peps/pep-0008/). Extract from it could be found in [this](http://stackoverflow.com/questions/6930144/underscore-vs-double-underscore-with-variables-and-methods) SO question. – twil Nov 09 '13 at 00:13
  • AFAIK, PEP 8 doesn't give us a convention for read-only variables. My understanding is that read-only variables are usually achieved by creating a non-public variable (with the underscore) then providing public, read-only access using properties. I think that's in line with what I'm trying to do in this question. Of course, the variable still isn't really read-only, the user can always write to the non-public variable directly if they choose. – Emma Nov 11 '13 at 16:16
  • Of course it doesn't. As you've said the variable still isn't read-only. As it said in PEP 8 underscore is "internal" variables and double underscore variable could be thinked of as "private". And it is definetely in line with what you are trying to do:) – twil Nov 11 '13 at 16:41
  • 1
    broken link for the `functools.partial` way, *sad face* – Amir Eldor Apr 17 '17 at 11:53