Design thought for callbacks

Cem Karan cfkaran2 at gmail.com
Sat Feb 21 10:45:22 EST 2015


On Feb 21, 2015, at 9:36 AM, Chris Angelico <rosuav at gmail.com> wrote:

> On Sun, Feb 22, 2015 at 1:07 AM, Cem Karan <cfkaran2 at gmail.com> wrote:
>> I agree about closures; its the only way they could work.  When I was originally thinking about the library, I was trying to include all types of callbacks, including closures and callable objects.  The callable objects may pass themselves, or one of their methods to the library, or may do something really weird.
>> 
>> Although I just realized that closures may cause another problem.  In my code, I expect that many different callbacks can be registered for the same event.  Unregistering means you request to be unregistered for the event. How do you do that with a closure?  Aren't they anonymous?
>> 
> 
> They're objects, same as any other, so the caller can hang onto a
> reference and then say "now remove this one". Simple example:
> 
> callbacks = []
> def register_callback(f): callbacks.append(f)
> def unregister_callback(f): callbacks.remove(f)
> def do_callbacks():
>    for f in callbacks:
>        f()
> 
> def make_callback(i):
>    def inner():
>        print("Callback! %d"%i)
>    register_callback(inner)
>    return inner
> 
> make_callback(5)
> remove_me = make_callback(6)
> make_callback(7)
> unregister_callback(remove_me)
> do_callbacks()

Yeah, that's pretty much what I thought you'd have to do, which kind of defeats the purpose of closures (fire-and-forget things).  BUT it does answer my question, so no complaints about it!

So, either you keep a reference to your own closure, which means that the library doesn't really need to, or the library keeps hold of it for you, in which case you don't have a reasonable way of removing it.

> The other option is for your callback registration to return some kind
> of identifier, which can later be used to unregister the callback.
> This is a good way of avoiding reference cycles (the ID could be a
> simple integer - maybe the length of the list prior to the new
> callback being appended, and then the unregistration process is simply
> "callbacks[id] = None", and you skip the Nones when iterating), and
> even allows you to register the exact same function more than once,
> for what that's worth.

That would work.  In the cases where someone might register & unregister many callbacks, you might use UUIDs as keys instead (avoids the ABA problem).

> When I do GUI programming, this is usually how things work. For
> instance, I use GTK2 (though usually with Pike rather than Python),
> and I can connect a signal to a callback function. Any given signal
> could have multiple callbacks attached to it, so it's similar to your
> case. I frequently depend on the GTK engine retaining a reference to
> my function (and thus to any data it requires), as I tend not to hang
> onto any inner objects that don't need retention. Once the parent
> object is destroyed, all its callbacks get dereferenced. Consider this
> simplified form:
> 
> def popup_window():
>    w = Window()
>    # Add layout, info, whatever it takes
>    btn = Button("Close")
>    w.add(btn) # actually it'd be added to a layout
>    btn.signal_connect("clicked", lambda *args: w.destroy())
> 
> The GUI back end will hang onto a reference to the window, because
> it's currently on screen; to the button, because it's attached to the
> window; and to my function, because it's connected to a button signal.
> Then when you click the button, the window gets destroyed, which
> destroys the button, which unregisters all its callbacks. At that
> point, there are no refs to the function, so it can get disposed of.
> That button function was the last external reference to the window,
> and now that it's not on screen, its Python object can also be
> disposed of, as can the button inside. So it'll all clean up fairly
> nicely; as long as the callback gets explicitly deregistered, that's
> the end of everything.

OK, so if I'm reading your code correctly, you're breaking the cycle in your object graph by making the GUI the owner of the callback, correct?  No other chunk of code has a reference to the callback, correct?  

Thanks,
Cem Karan


More information about the Python-list mailing list