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