4

The observer pattern in the very simplified code below works well. I would like to have have a decorator @on_event that does the registration in the Observable singleton.

In class O2 below this does not work. The problem is of course that the decorator on_event get's called prior to the instance is created, and the registration will be to the unbound method event. In some way I have to delay the registration until the O2 object is initialized. Maybe needless to say but all I want to add in O2 is the decorator as in the code below.

But for sure there must be a solution to this? I have googled around but cannot find anything, and have tried several approaches.

class Observable(object):
    _instance = None

    @classmethod
    def instance(cls):
        if not cls._instance:
            cls._instance = Observable()
        return cls._instance

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

    def event(self, data):
        for fn in self.obs:
            fn(data)

def on_event(f):
    def wrapper(*args):
        return f(*args)
    Observable.instance().obs.append(f)
    return wrapper

class O(object):

    def __init__(self, name):
        self.name = name
        Observable.instance().obs.append(self.event)

    def event(self, data):
        print self.name + " Event: " + data

class O2(object):

    def __init__(self, name):
        self.name = name

    @on_event
    def eventx(self, data):
        print self.name + " Event: " + data

if __name__ == "__main__":
    o1 = O("o1")
    Observable.instance().event("E1")

    o2 = O2("o2")
    Observable.instance().event("E2")
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
jorgen
  • 688
  • 6
  • 16
  • So what *instance* should the method be called on then, when the event fires? That is the issue here; you can only register bound methods if there is an instance to bind the method *to*. What are you trying to register here? The method of just one `O2` instance, all instances of `O2`, something else? – Martijn Pieters Jun 25 '14 at 16:12
  • @MartijnPieters in the example it should be called on the o2 instance - all instances of O2. (Exactly as O works) – jorgen Jun 25 '14 at 16:13
  • You cannot do that with a decorator alone; you'd need to set a metaclass on `O2` as well to hook into instance creation, then register the bound method every time you create an instance. – Martijn Pieters Jun 25 '14 at 16:16
  • Although theoretically the decorator could also insert a `__new__` or `__init__` method in to the same class namespace that then does hooking; but this gets ugly fast, and is not recommended. – Martijn Pieters Jun 25 '14 at 16:18
  • (see [How does the function that is called inside the class declaration?](http://stackoverflow.com/a/13330660) for how ugly that can get; that specific trick won't even work in Python 3). – Martijn Pieters Jun 25 '14 at 16:19
  • @MartijnPieters I would like to avoid that kind of solution. I hope someone has solved this. But I am starting to give up. – jorgen Jun 25 '14 at 16:21
  • You can use a metaclass, plus a decorator for the function; that requires that the user of your observer pattern library has to specify the metaclass as well as decorate the method. – Martijn Pieters Jun 25 '14 at 16:26

1 Answers1

5

You cannot register bound methods until you have an instance for the method to be bound to. A function decorator alone does not have the context to detect when an instance has been created.

You could use a metaclass / decorator combined approach instead:

class ObservingMeta(type):
    def __call__(cls, *args, **kw):
         instance = super(ObservingMeta, cls).__call__(*args, **kw)
         for attr in vars(cls).values():
             if hasattr(attr, '__observer__'):
                 # register bound method
                 bound = attr.__get__(instance, cls)
                 Observable.instance().obs.append(bound)
         return instance

This registers all methods directly defined on cls that are marked as observers; the marking is done with a decorator:

def on_event_method(f):
    f.__observer__ = True
    return f

This is then used as:

class O2(object):
    __metaclass__ = ObservingMeta

    def __init__(self, name):
        self.name = name

    @on_event_method
    def eventx(self, data):
        print self.name + " Event: " + data

Do note that storing the methods in your Observable singleton keeps the instances alive; if you create instances of O2 the bound eventx methods reference the instance, and by keeping references to the methods that means the instances are never garbage collected if all other references to them are deleted.

See using python WeakSet to enable a callback functionality for how you could use weak references instead to only keep tabs on methods until the underlying instance has been deleted, without keeping the instances alive indefinitely.

Community
  • 1
  • 1
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • This solution solves the problem stated in the question, however I really wanted a solution where the same decorator could be used on module level aswell. Reason beeing that the problem was that the decorator did not work in a class. – jorgen Jun 26 '14 at 05:05
  • Yes, but that'd require detecting you are in a class definition and adjusting how you register the function. See http://stackoverflow.com/questions/8793233/python-can-a-decorator-determine-if-a-function-is-being-defined-inside-a-class. Again, rather ugly, and *any* function can be turned into a method, really. – Martijn Pieters Jun 26 '14 at 07:33