I want to be able to tell wheter a data field in my Python object was modified since some event. I thought I would implement this using properties like the following:
class C(object):
def get_scores(self):
print 'getter'
return self.__scores
def set_scores(self, value):
print 'setter'
self.__scores = value
self.__scoresWereChanged = True
scores = property(get_scores, set_scores, None, None)
def do_something(self):
if self.__scoresWereChanged:
self.__updateState() #will also set self.__scoresWereChanged to False
self.__do_the_job()
This approach works well if scores
is immutable, but it scores
is a list and I change a single element in it, the approach fails:
In [79]: c.scores = arange(10)
setter
In [80]: print c.scores
getter
Out[80]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [81]: c.scores[3] = 999
getter #setter was not called
I can solve this by
In [84]: s = c.scores
getter
In [85]: s[3] = 2222
In [86]: c.scores = s
setter
but, of course this will not be as ellegant as I would like to.
It can be done - I can think of a complicated way, where each assignment to a "guarded" property would create a dynamic wrapper class to the object being stored. That class would monitor access to any "magic" method on the object (that is __setitem__, __setattr__, __iadd__, __isub__, and so on).
Creating such a class dynamically is fun, but tricky, and tricky to work right for every situation.
Another option I can think of, is to create a copy of each guarded property, and on each read access it would check to see if the property stored is still equal its copy. If not, it would mark it as dirty.
Instead of using properties, it will be cleaner to set up a descriptor class (which implements a "property" like behavior)
from copy import deepcopy
class guarded_property(object):
# self.name attribute set here by the metaclass
def __set__(self, instance, value):
setattr(instance, "_" + self.name, value)
setattr(instance, "_" + self.name + "_copy", deepcopy(value))
setattr(instance, "_" + self.name + "_dirty", True)
def __get__(self, instance, owner):
return getattr (instance, "_" + self.name)
def clear(self, instance):
setattr(instance, "_" + self.name + "_dirty", False)
setattr(instance, "_" + self.name + "_copy", deepcopy(self.__get__(instance, None)))
def dirty(self, instance):
return getattr(instance, "_" + self.name + "_dirty") or not (getattr(instance, "_" + self.name + "_copy") == self.__get__(instance, None))
class Guarded(type):
def __new__(cls, name, bases, dict_):
for key, val in dict_.items():
if isinstance(val, guarded_property):
val.name = key
dict_[key + "_clear"] = (lambda v: lambda self: v.clear(self))(val)
dict_[key + "_dirty"] = (lambda v: property(lambda self: v.dirty(self)))(val)
return type.__new__(cls, name, bases, dict_)
if __name__ == "__main__":
class Example(object):
__metaclass__ = Guarded
a = guarded_property()
g = Example()
g.a = []
g.a_clear()
print g.a_dirty
g.a.append(5)
print g.a_dirty
g.a_clear()
print g.a_dirty