Search code examples
pythonencapsulation

Logging access to elements in data members of python objects


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.


Solution

  • 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