Welcome, guest | Sign In | My Account | Store | Cart

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.

Python, 174 lines
  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

4 comments

RSquare 13 years, 3 months ago  # | flag

What python version did you use for this pattern?

Bob Marley 13 years, 1 month ago  # | flag

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.

Miki Tebeka 12 years, 9 months ago  # | flag
Rodney Drenth (author) 12 years, 8 months ago  # | flag

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.