Search code examples
pythoncallbackclosuresscoping

Future Proof Scoping In Callback Closures [Python]


We're generally familiar with this "gotcha" in Python due to it's scoping:

functions = []
for i in range(3):
    functions.append(lambda : i)

out = [f() for f in functions]

# naive expectation = [0, 1, 2]
# actual result = [2, 2, 2]

And we're generally familiar with how to match our expectations:

    functions.append(lambda i=i: i)

However, while trying to create "future proof" callback closures, I've run into a problem where the possibility of an expanding callback signature would break the default arguments used to define the scope of the closure.


Consider this case:

def old_style_callback(data, *args, **kwargs):
    # ...

# define a bunch of closures
closures = []
for cb in list_of_callbacks:
    def old_style_closure(data, callback=cb, *args, **kwargs):
        cb(data, *args, **kwargs)
        # ...
    closures.append(old_style_closure)

But what if you need to add a new argument that accommodates some new functionality?

def new_style_callback(data, metadata, *args, **kwargs):
    # ...

Now your old_style_closure has been broken, since metadata will get passed to the default argument you used to extend the closure's scope and access the callback!


It seems like if you want "future proof" callback closures, you're forced to stick with your original signature and just pass everything through those arguments. That isn't so terrible, but this presents a problem if you didn't make your original signature generic enough.

Any thoughts or new approaches to this problem are appreciated.


Solution

  • After writing this question I came to the realization that you can solve this problem by creating a new function with it own scope to create the closure. Once you have that, you just call it in-loop:

    def create_callback_closure(callback):
        def old_style_closure(data, *args, **kwargs):
            callback(data, *args, **kwargs)
            # ...
    
    closures = []
    for cb in list_of_callbacks:
        closures.append(create_callback_closure(callback))