What's the preferred way to implement a hook or callback in Python?

kindall picture kindall · Nov 30, 2010 · Viewed 15.9k times · Source

I'd like to provide the capability for users of one of my modules to extend its capabilities by providing an interface to call a user's function. For example, I want to give users the capability to be notified when an instance of a class is created and given the opportunity to modify the instance before it is used.

The way I've implemented it is to declare a module-level factory function that does the instantiation:

# in mymodule.py
def factory(cls, *args, **kwargs):
    return cls(*args, **kwargs)

Then when I need an instance of a class in mymodule, I do factory(cls, arg1, arg2) rather than cls(arg1, arg2).

To extend it, a programmer would write in another module a function like this:

def myFactory(cls, *args, **kwargs):
    instance = myFactory.chain(cls, *args, **kwargs)
    # do something with the instance here if desired
    return instance

Installation of the above callback looks like this:

myFactory.chain, mymodule.factory = mymodule.factory, myFactory

This seems straightforward enough to me, but I was wondering if you, as a Python programmer, would expect a function to register a callback rather than doing it with an assignment, or if there were other methods you would expect. Does my solution seem workable, idiomatic, and clear to you?

I am looking to keep it as simple as possible; I don't think most applications will actually need to chain more than one user callback, for example (though unlimited chaining comes "for free" with the above pattern). I doubt they will need to remove callbacks or specify priorities or order. Modules like python-callbacks or PyDispatcher seem to me like overkill, especially the latter, but if there are compelling benefits to a programmer working with my module, I'm open to them.

Answer

Ignacio Vazquez-Abrams picture Ignacio Vazquez-Abrams · Nov 30, 2010

Taking aaronsterling's idea a bit further:

class C(object):
  _oncreate = []

  def __new__(cls):
    return reduce(lambda x, y: y(x), cls._oncreate, super(C, cls).__new__(cls))

  @classmethod
  def oncreate(cls, func):
    cls._oncreate.append(func)

c = C()
print hasattr(c, 'spew')

@C.oncreate
def spew(obj):
  obj.spew = 42
  return obj

c = C()
print c.spew