The observer pattern is implemented using an observable descriptor. One creates instances of observable in a class, which allows observers to subscribe to changes in the observable. Assigning a value to the observable causes the suscribers to be notified. An observable can subscribe to another observable, in which case changes to the second propagate to subscribers of the first. The subscribe method returns a Subscription object. When this object is deleted or becomes unreferenced, the subscription is cancelled.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | import weakref
import exceptions
import threading # for lock
class RecursionError(exceptions.RuntimeError):
pass
class _subscription(object):
"""A subscription is returned as a result of subscribing to an
observable. When the subscription object is finalized, the
subscription is cancelled. This class is used to facilitate
subscription cancellation."""
def __init__(self, subscriber, observed):
self.subscriber = subscriber
self.observed = weakref.ref(observed)
def __del__(self):
obsrvd = self.observed()
if (self.subscriber and obsrvd):
obsrvd._cancel(self.subscriber)
class _obwan(object):
'''Half-hidden class. Only 'observable should construct these.
Calls to subscribe, cancel get invoked through the observable.
_obwan objects reside in class instances containing observables.'''
def __init__(self):
self.subscribers = []
self._value = None
self._changeLock = threading.Lock()
def __call__(self):
"""returns the current value, the one last set"""
return self._value
def _notifySubscribers(self, value):
for (f,exceptionHdlr) in self._callbacks():
try:
f(value)
except Exception, ex:
if exceptionHdlr and not exceptionHdlr(ex):
raise # reraise if not handled
def setvalu(self, value):
"""Notify the subcribers only when the value changes."""
if self._value != value:
if self._changeLock.acquire(0): # non-blocking
self._value = value
try:
self._notifySubscribers(value)
finally:
self._changeLock.release()
else:
raise RecursionError("Attempted recursion into observable's set method.")
def subscribe(self, obsv, exceptionInfo = None):
observer = obsv.setvalu if isinstance(obsv, _obwan) else obsv
ob_info =(observer, exceptionInfo)
self.subscribers.append(ob_info)
return _subscription(ob_info, self)
def _callbacks(self):
scribers = []
scribers.extend(self.subscribers)
return scribers
def _cancel(self, wref):
self.subscribers.remove(wref)
class Observable(object):
"""An observable implemented as a descriptor. Subscribe to an observable
via calling xxx.observable.subscribe(callback)"""
def __init__(self, nam):
self.xname = "__"+nam
self.obwan = _obwan
def __set__(self,inst, value ):
"""set gets the instances associated variable and calls
its setvalu method, which notifies subribers"""
if inst and not hasattr(inst, self.xname):
setattr(inst, self.xname, self.obwan())
ow = getattr(inst, self.xname)
ow.setvalu(value)
def __get__(self, inst, klass):
"""get gets the instances associated variable returns it"""
if inst and not hasattr(inst, self.xname):
setattr(inst, self.xname, self.obwan())
return getattr(inst, self.xname)
#-----------------------------------------------------------------------
# Example & Simple Test
#-----------------------------------------------------------------------
if __name__ == '__main__':
class MyClass(object):
""" A Class containing the observables length and width"""
length = Observable('length')
width = Observable('width')
def __init__(self):
self.length.setvalu(0)
self.width.setvalu(0)
class MyObserver(object):
"""An observer class. The initializer is passed an instance
of 'myClass' and subscribes to length and width changes.
This observer also itself contains an observable, l2"""
l2 = Observable('l2')
def __init__(self, name, observedObj):
self.name = name
self.subs1 = observedObj.length.subscribe(self.print_l)
self.subs2 = observedObj.width.subscribe(self.print_w)
"""An observable can subscribe to an observable, in which case
a change will chain through both subscription lists.
Here l2's subscribers will be notified whenever observedObj.length
changes"""
self.subs3 = observedObj.length.subscribe(self.l2)
def print_w(self, value):
print "%s Observed Width"%self.name, value
def print_l(self, value):
print "%s Observed Length"%self.name, value
def cancel(self):
"""Cancels the instances current subscriptions. Setting self.subs1 to
None removes the reference to the subscription object, causing it's
finalizer (__del__) method to be called."""
self.subs1 = None
self.subs2 = None
self.subs3 = None
def pl2(value):
print "PL2 reports ", value
if type(value) == type(3):
raise ValueError("pl2 doesn't want ints.")
def handlePl2Exceptions( ex ):
print 'Handling pl2 exception:', ex, type(ex)
return True # true if handled, false if not
area = MyClass()
kent = MyObserver("Kent", area)
billy = MyObserver("Billy", area)
subscription = billy.l2.subscribe(pl2, handlePl2Exceptions)
area.length = 6
area.width = 4
area.length = "Reddish"
billy.subs1 = None
print "Billy shouldn't report a length change to 5.15."
area.length = 5.15
billy.cancel()
print "Billy should no longer report"
area.length = 7
area.width = 3
print "Areas values are ", area.length(), area.width()
print "Deleting an object with observables having subscribers is ok"
area = None
area = MyClass()
print "replaced area - no subscribers to this new instance"
area.length = 5
area.width ="Bluish"
c = raw_input("ok? ")
|
In the example, MyClass contains two observables, length and width. An observer class is defined. The second argument to the initializer is an instance of MyClass. The initializer subscribes to changes in myClass's length and width values.
The observer class itself contains an observable, l2. And the observer's init method sets the l2 observable to be a subscriber to the length attribute of the myClass instance.
In the example two instances of the observer class are created and subscribe to changes of the length and width of the area object.
- ??-Dec-09 Modified to use weak reference in subscriber list.
- 11-Mar-10 Fix problems introduced with using weakref and simplify subscription cancellation. Improve variable names. Simplify examples.
- 12-Mar-10 Fix for when subscribers unsubsribe themselves during a notification and for when object containing observables is deleted while having subscribers
- 13-Mar-10 Improve exception handling, changes for use with threadsafe version, docstring improvements
What python version did you use for this pattern?
Took me a little while to figure out this recipe. Looks pretty good!
For the benefit of others trying to figure it out, this is based on the "Design Pattern" called "Observer". (In case you need background information, use Google to look up "Design Patterns" and "Gang of four" (or GoF), to see a mid-1990's book which listed a bunch of design patterns discovered in various software.)
The Observer pattern is one where an observer can register (subscribe) to be notified anytime a certain object changes. As a simple, real life example, in Windows, if you are viewing a directory (folder) using Windows Explorer, and some other program or user changes the directory's contents, your view of the directory updates automatically. This is because the Windows Explorer program has subscribed to that particular folder, wanting to be notified of any changes. (Event-driven software such as Windows Graphical User Interfaces have callbacks and notifications in them in many places.)
The naming conventions used in this Recipe are a little weird. _obwan? I assume this is some sort of Star Wars reference (obi wan kenobi?)- or maybe a play on the first 2 letters of "observable". l2 and pl2 - I think these are for "Level 2" and "Print Level 2", but that's just a guess.
In the test/example, one of the Observers actually establishes another 2nd level observable, so that people can notified in the change of the "length" variable, so that it can do some data validation. This is the L2 (Level 2) subscription. User "billy" subscribes to changes in the "area" field, but also subscribes to the length changes (and data validation) at a second level. This is different than the user "kent", who simply subscribes to changes in the area.
Hope this description helps someone.
I think this is a cool recipe, but you should also check out the thread-safe version at 577106.
There's also http://code.enthought.com/projects/traits/
I believe the code was written for Python 2.6. I think it will work with 2.4 & 2.5 though.
Yes, _obwan is rather obtuse. Got a better name for it? The ob came from observable, the rest is star wars.
I'll have a look at recipe 577106.