November 5, 2004

Closures in Python (part 3)

Just a short addition to part 2 - I was writing some code the other day and came across another closure related problem with Python. I wanted to create some buttons, with event callbacks, dynamically; something like this:


class Foo:
   def __init__(self):
      self.callbacks = [lambda event: self.callback(name) for name in ['bob','bill','jim']]
   def callback(self, name):
      print name
   def run(self):
      for callback in self.callbacks:
         callback("some event")

Now, what do you suppose happens when you run "Foo().run()"?
You get:


jim
jim
jim

This is because there is only one "name" variable (so Nat Pryce tells me). A simple solution is to do:


class Foo:
   def __init__(self):
      self.callbacks = [self.makeCallback(name) for name in ['bob','bill','jim']]
   def makeCallback(self, name):
      return lambda event: self.callback(name)
   def callback(self, name):
      print name
   def run(self):
      for callback in self.callbacks:
         callback("some event")

I'm sure there are many other solutions too. For the curious, in Ruby the equivalent code would be:


class Foo
   def initialize()
      @callbacks = ['bob','bill','jim'].map {|name| Proc.new {|event| callback(name)}}
   end
   def callback(name)
      print name
   end   
   def run()
      @callbacks.each { |callback| callback.call("some event") }
   end
end

Foo.new.run()

which works just fine. However, I still chose Python over Ruby for pragmatic reasons (a bit ironic I know) - the libraries, tools and support are all superior.

Posted by ivan at November 5, 2004 11:45 PM
Copyright (c) 2004-2008 Ivan Moore
Comments

The standard python solution to this issue is to pass a second parameter to lambda and give it a default parameter of the variable you want bound into the function.
e.g. replace

self.callbacks = [lambda event: self.callback(name) for name in ['bob','bill','jim']]

with

self.callbacks = [lambda event, name=name: self.callback(name) for name in ['bob','bill','jim']]

this will now work the way you want.

Posted by: Dave Kirby at November 6, 2004 10:55 AM

Many thanks Dave - I've seen this before but forgot. Either way it's not very nice.

I was wondering if there might be some magic with new style classes such that you could implement partially evaluated functions. Then the methods "makeCallback" and "callback" could be replaced by simply a single method "def callback(self, name, event):..." such that "self.callback(name)" returns a function expecting just the missing "event" parameter. Being the Python wizard you are, this might be an interesting challenge? Then the relevant line would be (or at least I'd like it to be):

self.callbacks = [self.callback(name) for name in ['bob','bill','jim']]

Or is this more confusing?

Posted by: ivan at November 6, 2004 1:55 PM

There are several implementations of function currying in Python. The following three are all on the ASPN Python cookbook, an invaluable resource for problems like this.

http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52564

http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549

http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/222061

Do any of these do what you want?

Posted by: Dave Kirby at November 9, 2004 10:40 PM