Observer-Pattern by (simple) decorator

David Wahler dwahler at gmail.com
Sat Jun 2 10:39:47 EDT 2007


On Jun 2, 3:00 pm, Peter Otten <__pete... at web.de> wrote:
<snip>
> Then you have modified the code posted by Steven Bethard.
>
> > I don't see how your behaviour should come about ... a new observer-list
> > is created for every decorated method, so there is no problem.
>
> Yes, but that list is shared across instances of SomeActor.
>
> > Now I'm confused ?-|
>
> You start with one Observable per method:
>
> >>> SomeActor.meth
>
> <tmp.Observable object at 0x401d5b0c>>>> SomeActor.meth is SomeActor.meth
>
> True
>
> This Observable knows nothing about a SomeActor instance (because there is
> none).
>
> >>> SomeActor.meth.instance is None
>
> True
>
> Everytime you access meth from a SomeActor instance the __get__() method is
> invoked and a new Observable is created, this time with an instance:
>
> >>> SomeActor().meth is SomeActor.meth
> False
> >>> SomeActor().meth.instance
>
> <tmp.SomeActor object at 0x401d56ec>
>
> But all of these Observables share the same observables list
>
> >>> SomeActor().meth.observers is SomeActor.meth.observers
> True
> >>> SomeActor().meth.observers is SomeActor().meth.observers
>
> True
>
> If you want per-instance callbacks you have to store the observers list (or
> the bound method) in the instance:
>
> >>> class SomeActor(object):
>
> ...     def __init__(self):
> ...             self.meth = Observable(self.meth, self)
> ...     def meth(self, foo): print foo
> ...>>> a = SomeActor()
> >>> b = SomeActor()
> >>> def make_callback(s):
>
> ...     def f(instance):
> ...             print s, instance
> ...     return f
> ...>>> a.meth.add_callback(make_callback("alpha"))
> >>> b.meth.add_callback(make_callback("beta"))
> >>> a.meth(1)
>
> 1
> alpha <__main__.SomeActor object at 0x401d5c6c>>>> b.meth(2)
>
> 2
> beta <__main__.SomeActor object at 0x401d5ccc>
>
> Note that with this approach Observable need not be a descriptor; I was just
> too lazy to rewrite it.

Here's my attempt at an implementation that works as a decorator. It
stores the observers in a dictionary attribute of the instance, using
the method name as a key. For example:

>>> a = SomeActor()
>>> a.meth.add_callback(callback)
>>> a._observers
{'meth': [<function callback at 0x00C29870>]}
>>> a.meth.observers is a._observers['meth']
True

######################################################################

class Observable(object):
    def __init__(self, func, instance=None):
        self.func = func
        self.instance = instance
        if instance is not None and not hasattr(self.instance,
'_observers'):
            self.instance._observers = {}

    def __get__(self, obj, cls=None):
        if obj is None:
            return self
        else:
            func = self.func.__get__(obj, cls)
            return Observable(func, obj)

    def __call__(self, *args, **kwargs):
        result = self.func(*args, **kwargs)
        for observer in self.observers:
            observer(self.instance)
        return result

    @property
    def observers(self):
        func_name = self.func.im_func.func_name
        return self.instance._observers.setdefault(func_name,[])

    def add_callback(self, callback):
        self.observers.append(callback)

######################################################################

This implementation has a few drawbacks that I can see:

  * It doesn't allow for the syntax SomeActor.meth(instance, *args).
    I haven't spent the time to make it work on unbound as well as
    bound methods.

  * It may not play well with inheritance. If the subclass overrides
    an Observable method of the superclass, and declares its override
    Observable as well, the callback will be called twice. I'm not
    sure exactly how to handle this.

  * Perhaps it would be better to use the function object itself as
    the dictionary key, rather than its name?

Anyway, these are just my initial thoughts -- I don't have the time to
really think this through thoroughly.

-- David




More information about the Python-list mailing list