Giuseppe Pes
Giuseppe Pes

Reputation: 7912

Bind callback to the object instance

I'd like to bind a class method to the object instance so that when the method is invoke as callback it can still access the object instance. I am using an event emitter to generate and fire events.

This is my code:

#!/usr/bin/env python3
from pyee import EventEmitter

class Component(object):
  _emiter = EventEmitter()

  def emit(self, event_type, event):
    Component._emiter.emit(event_type, event)

def listen_on(event):
  def listen_on_decorator(func):
    print("set event")
    Component._emiter.on(event, func)
    def method_wrapper(*args, **kwargs):
      return func(*args, **kwargs)
    return method_wrapper
  return listen_on_decorator

class TestComponent(Component):

  @listen_on('test')
  def on_test(self, event):
    print("self is " + str(self))
    print("FF" + str(event))

if __name__ == '__main__':
  t = TestComponent()
  t.emit('test', { 'a': 'dfdsf' })

If you run this code, an error is thrown :

  File "component.py", line 29, in <module>                                                                                                                                                         [0/1889]
    t.emit('test', { 'a': 'dfdsf' })
  File "component.py", line 8, in emit
    Component._emiter.emit('test', event)
  File "/Users/giuseppe/.virtualenvs/Forex/lib/python3.4/site-packages/pyee/__init__.py", line 117, in emit
    f(*args, **kwargs)
  File "component.py", line 14, in method_wrapper
    return func(*args, **kwargs)
TypeError: on_test() missing 1 required positional argument: 'event'

This is caused by the missing self when the method on_test is called.

Upvotes: 1

Views: 3074

Answers (2)

jsbueno
jsbueno

Reputation: 110301

Based on OP's extra requierements presented on the comments to the other answer, there is this alternative approach.

Here, the decorator is used only to mark which methods will work as emitters - and the actual registry of emitters is done only when the class is actually instantiated, based on the methods that were marked.

#!/usr/bin/env python3
from pyee import EventEmitter

class Component(object):
    _emiter = EventEmitter()
    def __init__(self):
        for attr_name in dir(self):
            method = getattr(self, attr_name)
            if hasattr(method, "_component_emitter_on"):
                for event in method._component_emitter_on:
                    self._emiter.on(event, method)
            self.attr_name = method

    def emit(self, event_type, event):
        Component._emiter.emit(event_type, event)


def listen_on(event):
    def listen_on_decorator(func):
        print("set event")
        func._component_emitter_on = getattr(func, "_component_emitter_on", []) + [event]
        return func
    return listen_on_decorator


class TestComponent(Component):

    @listen_on('test')
    def on_test(self, event):
        print("self is " + str(self))
        print("FF" + str(event))

if __name__ == '__main__':
    t = TestComponent()
    t.emit('test', { 'a': 'dfdsf' })

(Note I had also removed a redundant indirection level on your decorator - if decorators won't replace the callable itself, just make annotations to it (or with it), they don't need to create another callable)

Upvotes: 1

jsbueno
jsbueno

Reputation: 110301

The instance-bound method does not exist, as it is hard to imagine otherwise, at the time the class body is parsed - which is when decorators are applied.

That means your on_test method behaves just like a function, not being "aware" of it's class or instance at that point. When a method is retrieved from an object instance, Python does create an special callable (with "method" type) that essentially will insert the self parameter in a call to the original function.

One way to make it work is to decorate this callable (the bound method itself) instead of the raw function. Of course, it only exists at the time the class is instantiated.

Fortunately, in Python, decorators are mostly a syntactic shortcut to a function call passing the decorated function as a parameter - so you can just rewrite your TestComponent class more or less like this:

class TestComponent(Component):

  def __init__(self, *args, **kw):
      super().__init__(*args, **kw)
      self.on_test = listen_on('test')(self.on_test)

  def on_test(self, event):
    print("self is " + str(self))
    print("FF" + str(event))

Note that as we re-assign the self.on_test instance member after decoration, it ceases to behave as a method when called through the instance - it will be a mere function call, without magic inserting of self - however, the self parameter is already bound to that callable on the moment self.on_test is retrieved and passed as a parameter to the decorator, on the right side of that line.

** alternative ** The comments suggests the above use does not look like the decorator is used - one can rewrite that to actually use the decorator syntax, just doing this - although this will hide the method from static code checkers, such as IDE autocompletion engines, and linters:

class TestComponent(Component):

  def __init__(self, *args, **kw):
      super().__init__(*args, **kw)

      @listen_on('test')
      def on_test(self, event):
          print("self is " + str(self))
          print("FF" + str(event))

      self.on_test = on_test  

Upvotes: 1

Related Questions