Decorating a class to monitor attribute changes

Question:

I want to have classes that automatically send notifications to subscribers whenever one of their attributes change. So if I would write this code:

@ChangeMonitor
class ChangingClass(object):

    def __init__(self, x):
        self.x = x


changer = ChangingClass(5)
print("Going to change x.")
changer.x = 6
print("Going to not change x.")
changer.x = 6
print("End of program")

The output would be:

Going to change x
Old x = 5, new x = 6
Going to not change x.
End of program.

My question is how to implement the ChangeMonitor decorator class. In the above example I assume it will print a line indicating the changes of an attribute, but for useful purposes it could send notifications to subscribed objects.

Asked By: user2302084

||

Answers:

You’d have to add a __setattr__() method:

def ChangeMonitor(cls):
    _sentinel = object()
    old_setattr = getattr(cls, '__setattr__', None)
    def __setattr__(self, name, value):
        old = getattr(self, name, _sentinel)
        if old not is _sentinel and old != value:
            print "Old {0} = {1!r}, new {0} = {2!r}".format(name, old, value)
        if old_setattr:
            old_setattr(self, name, value)
        else:
            # Old-style class
            self.__dict__[name] = value

    cls.__setattr__ = __setattr__

    return cls

This should handle existing __setattr__ hooks as well. The _sentinel is used to allow None as the old value too.

Demo:

>>> changer = ChangingClass(5)
>>> changer.x = 6
Old x = 5, new x = 6
>>> changer.x = 6
>>> # nothing printed
...
>>> changer.x = None
Old x = 6, new x = None
>>> changer.x = 6
Old x = None, new x = 6
Answered By: Martijn Pieters

This answer is based on the Answer by Martijn Pieters, I found it useful, but I updated it to fit better with Python3.

Like the accepted answer I’ve overridden the __setattr__ special method, but we save most of the complexity by just using super to keep the original behavior of the method.

def ChangeMonitor(cls):
    def __setattr__(self, name, value):
        try:
            original_value = getattr(self, name) 
            if original_value == value:
                print(f"No Change for Value {name}")
            else:
                print(f"Value {name} has changed from {original_value} to {value}")
        except AttributeError: 
            print(f"Initializing Value {name} with {value}")
        super(cls, self).__setattr__(name, value)
    cls.__setattr__ = __setattr__
    return cls

Demo:

>>> changer = ChangingClass(5)
Initializing Value x with 5
>>> changer.x = 6
Value x has changed from 5 to 6
>>> changer.x = None
Value x has changed from 6 to None
>>> changer.x = 2
Value x has changed from None to 2
>>> changer.x += 8
Value x has changed from 2 to 10

There are a few edge cases that if you’re relying on this you should consider.

If your datatype is mutable you can still change the underlying data without triggering setattr.

>>> changer = ChangingClass([5])
Initializing Value x with [5]
>>> changer.x.append(10)
>>> changer.x = []
Value x has changed from [5, 10] to []

In addition class variables changing will not trigger your setattr method. And if you override the class variable with an instance variable, it won’t register as initializing.

>>> changer = ChangingClass(1)
Initializing Value x with 1
>>> changer.z = 9
Initializing Value z with 9
>>> ChangingClass.y = 2
>>> changer.y = 6
Value y has changed from 2 to 6
Answered By: Antoine Wood