Search code examples
pythoncallbackclosurespartial-application

I need to provide a callback when instantiating a class from an API. How can I bind the instance itself to the callback, eagerly?


I've encountered this problem with a few different major third-party libraries and frameworks now. Let me try to boil it down to the essentials:

  • The API provides a class Example, where the constructor expects a callback parameter. When some event occurs (due to complex logic outside my control), the API will call the callback function.
  • I have a function modify that accepts an instance of Example and calls various methods on it:
    def modify(it):
        it.enabled = True
        it.visible = True
        try:
             it.paint('black')
        except AProblemComesAlong:
             it.whip()
    
  • I want to create an instance x of Example. When an event occurs that is associated with x, the x instance should be modified via modify.

Thus, I would like to bind x as an argument to modify, per Python Argument Binders. The problem is, it doesn't exist yet, because I am still calling the constructor:

>>> from functools import partial
>>> x = Example(callback=partial(modify, x))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined

Of course, I could avoid the NameError by allowing the lambda to look up the name later:

>>> x = Example(callback=lambda: modify(x))

but this is late binding, so it doesn't work properly if e.g. I'm in a loop and instance is the iteration variable, or if instance is reassigned later for any other reason.

How can I accomplish early binding of instance to its own callback?


Solution

  • Generally, you can try any of these approaches:

    • Double-check if the API allows you to set the callback later (two-phase construction):
      from functools import partial
      instance = Example()
      # Read the documentation and see if Example provides something like:
      instance.callback = partial(modify, instance)
      # or possibly they didn't think of using a decorator for their validation logic:
      instance.set_callback(partial(modify, instance))
      
    • Subclass the example so that it calls the callback from its own method, and adjusts the construction arguments to use that method as a wrapper:
      from functools import partial
      
      class ContextProvidingExample(Example):
          def __init__(self, *args, **kwargs):
              try:
                  my_callback = kwargs['callback']
                  kwargs['callback'] = partial(my_callback, self)
              except KeyError:
                  pass
              super().__init__(*args, **kwargs)
      
      Credit @tdelaney for the idea here.
    • If the flexibility isn't needed, the modify logic could be integrated directly into the subclass instead:
      class SelfModifyingExample(Example):
          def __init__(self, *args, **kwargs):
              if 'callback' in kwargs.keys():
                  raise ValueError('cannot override callback')
              kwargs['callback'] = self._modify
              super().__init__(*args, **kwargs)
          def _modify(self):
              self.enabled = True
              self.visible = True
              try:
                  self.paint('black')
              except AProblemComesAlong:
                  self.whip()
      
    • As a last resort, register instances in a dictionary, and arrange for the callback to look them up by name:
      from functools import partial
      
      hey_you = {} # say my name...
      def modify_by_name(name):
          modify(hey_you[name]) # call modify() maybe?
      # Let's use a simple wrapper to make sure instances get registered.
      def stand_up(name):
          result = Example(callback=partial(modify_by_name, name))
          hey_you[name] = result
          return result
      
      who = what = stand_up('slim shady')
      
      This way is a bit clunky, but you may find the string name for instances useful elsewhere in the code.