[Persistence-sig] A simple Observation API

Phillip J. Eby pje@telecommunity.com
Tue, 23 Jul 2002 21:01:24 -0400


I've taken the time this evening to draft a simple Observation API, and an 
implementation of it.  It's not well-documented, but the API should be 
fairly clear from the example below.  Comments and questions encouraged.

Note that this draft doesn't deal with any threading issues whatsoever.  It 
also doesn't try to address the possibility that an observer might throw an 
exception when it's given a notification during a 'finally' clause that 
closes a beginWrite/endWrite pair.  If anybody has suggestions for how to 
handle these situations, please let me know.

By the way, my informal tests show that subclassing Observable makes an 
object's attribute read access approximately 11 times slower than normal, 
even if no actual observation is taking place (i.e., an _o_readHook is not 
set).  I have not yet done a timing comparison for write operations and 
method calls, but I expect the slowdown to be as bad, or worse.  Rewriting 
Observation.py in C, using structure slots for many of the attributes, 
would probably eliminate most of these slowdowns, at least for unobserved 
instances.  Of course, any operations actually performed by a change 
observer or read hook, would add their own overhead, in addition to the raw 
observation overhead.

This is a fairly "transparent" API, although it still requires the user to 
subclass a specific base, and declare which mutable attributes are touched 
by what methods.  But it is less invasive, in that observation-specific 
code does not need to be incorporated into the methods themselves.

One possible enhancement to this framework: use separate observer lists for 
the beforeChange() and afterChange() events, and make them simple callables 
instead of objects with obvservation methods.  While this would require an 
additional attribute, it would simplify the process of creating dynamic 
activation methods, and reduce calls in situations where only one event 
needed to be captured.  This could be useful for setting up observation on 
a mutable attribute so as to "wire" it to trigger change events on the 
object(s) that contained it.

Anyway, here's the demo, followed by the module itself.


#### Demo of observation API ####

from Observation import Observable, WritingMethod

class aSubject(Observable):

     def __init__(self):
         self.spam = []

     # __init__ touches spam, but shouldn't notify anyone about it
     __init__ = WritingMethod(__init__, ignore=['spam'])


     def addSpam(self,spam):
         self.spam.append(spam)

     # addSpam touches spam, even though it doesn't set the attribute
     addSpam = WritingMethod(addSpam, attrs=['spam'])


     def setFoo(self, foo):
         self.foo = foo
         self.bar = 3*foo

     # setFoo modifies multiple attributes, and should send at most
     # one notice of modification, upon exiting.
     setFoo = WritingMethod(setFoo)


class anObserver(object):

     def beforeChange(self, ob):
         print ob,"is about to change"

     def afterChange(self, ob, attrs):
         print ob,"changed",attrs

     def getAttr(self, ob, attr):
         print "reading",attr,"of",ob
         return object.__getattribute__(ob,attr)


subj = aSubject()
obs  = anObserver()

subj._o_changeObservers = (obs,)
subj._o_readHook = obs.getAttr

subj.setFoo(9)
print subj.bar

subj.addSpam('1 can')

#####  End sample code #####



#### Observation.py ####

__all__ = ['Observable', 'WritingMethod', 'getAttr', 'setAttr', 'delAttr']

getAttr = object.__getattribute__
setAttr = object.__setattr__
delAttr = object.__delattr__

class Observable(object):

     """Object that can send read/write notifications"""

     _o_readHook = staticmethod(getAttr)
     _o_nestCount = 0
     _o_changedAttrs = ()
     _o_observers = ()

     def _o_beginWrite(self):

         """Start a (possibly nested) write operation"""

         ct = self._o_nestCount
         self._o_nestCount = ct + 1

         if ct:
             return

         for ob in self._o_changeObservers:
             ob.beforeChange(self)


     def _o_endWrite(self):

         """Finish a (possibly nested) write operation"""

         ct = self._o_nestCount = self._o_nestCount - 1

         if ct:
             return

         ca = self._o_changedAttrs

         if ca:
             del self._o_changedAttrs
             for ob in self._o_changeObservers:
                 ob.afterChange(self,ca)


     def __getattribute__(self,attr):

         """Return an attribute of the object, using a read hook if 
available"""

         if attr.startswith('_o_') or attr=='__dict__':
             return getAttr(self,attr)

         return getAttr(self,'_o_readHook')(self, attr)


     def __setattr__(self,attr,val):

         if attr.startswith('_o_') or attr=='__dict__':
             setAttr(self,attr,val)

         else:
             self._o_beginWrite()

             try:
                 ca = self._o_changedAttrs

                 if attr not in ca:
                     self._o_changedAttrs = ca + (attr,)

                 setAttr(self,attr,val)

             finally:
                 self._o_endWrite()


     def __delattr__(self,attr):

         if attr.startswith('_o_') or attr=='__dict__':
             delAttr(self,attr)

         else:
             self._o_beginWrite()

             try:
                 ca = self._o_changedAttrs
                 if attr not in ca:
                     self._o_changedAttrs = ca + (attr,)

                 delAttr(self,attr)
             finally:
                 self._o_endWrite()


from new import instancemethod

class WritingMethod(object):

     """Wrap this around a function to handle write observation 
automagically"""

     def __init__(self, func, attrs=(), ignore=()):
         self.func = func
         self.attrs = tuple(attrs)
         self.ignore = tuple(ignore)

     def __get__(self, ob, typ=None):

         if typ is None:
            typ = type(ob)

         return instancemethod(self, ob, typ)

     def __call__(self, inst, *args, **kwargs):

         attrs, remove = self.attrs, self.ignore

         inst._o_beginWrite()

         try:
             if attrs or remove:
                 ca = inst._o_changedAttrs
                 remove = [(r,1) for r in remove if r not in ca]
                 inst._o_changedAttrs = ca + attrs

             return self.func(inst, *args, **kwargs)

         finally:
             if remove:
                 inst._o_changedAttrs = tuple(
                     [a for a in inst._o_changedAttrs if a not in remove]
                 )
             inst._o_endWrite()