1

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?

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153

1 Answers1

1

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.
Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153