I got trapped by a lambda function
I consider myself a medium-to-experienced Python programmer (I am using python for 6 years). Today I've lost almos one hour in a very stupid bug with lambda functions. See what happend:
Tkinter, Buttons and Callbacks
I was building a very simple Tkinter GUI (I'm not an GUI expert, so I code in Tkinter because it is easy and simple, but I myself think that it is ugly).
In a moment it was necessary to create a lot of buttons (not a lot, but as I'm a lazy programmer, more than three is a lot). So I put it in a loop:
self.buttons = ( Button(self, text=b, command=self.click) for b in ['Install', 'Uninstall', 'Delete'])
Here you can see the problem: When I click the button, the function self.click will be called and the function itself cannot distinguish which button was clicked. A trick is to wrap the callback in a lambda function passing the button name:
self.buttons = ( Button(self, text=b, command=lambda:self.click(b)) for b in ['Install', 'Uninstall', 'Delete'])
In this way, When I click the button the function that will be called is the lambda function, that will make the call to my self.click passing the button name as an argument.
But it was not working to me. Instead of getting the name of the clicked button I was aways getting the last name declared in the list ('Delete' in my case).
To illustrate what was happening, take a look at the code bellow. The class 'A' mimics the buttons function. The 'click' function mimics a click on the button and call its proper function (passed as the argument 'command' in the class constructor).
To create the objects I'm using the same approach: passing a lambda function that wraps the function to be called.
k = [A(i, command=lambda:p(i)) for i in ('a', 'b', 'c')]
And, again, it doesn't matter which instance of 'A' I use, when I do .click() it will aways print 'c' (which is the name of the last instance in the list).
It took me a while to realize what was my mistake in this code. At first it looks okay, and I thought "when I pass the lambda to the 'command' argument I am aways passing the current value of the object name by th 'i' variable".
Well, that is not true. The lambda function will not evaluate the 'i' variable when it is passed to the class constructor. So every time I iterate over the list, I change the value of 'i'. This 'i' will be evaluated latter (I'm not pretty sure when, if you know just let me know in the comments).
So, when 'i' get evaluated, it's value has already changed...
The solution is pretty simple: we must force the evaluation to happen when I'm instantiating 'A', to assure that it will be evaluated with the current value of 'i'.
One way to do this is to pass that value of 'i' as a parameter of my lambda. So, instead of doing this:
lambda : self.click(i)
I did this:
lambda arg=i : self.click(arg)
This last lambda statement create a lambda function that accepts a parameter ('arg' is its name). The parameter has a default value, that is 'i'. It forces 'i' to be evaluated, and passed to the lambda function as a default value. So I can just call the lambda function without any argument and the default one takes the place.
The code below summarize the idea of this trick.
Don't be shy: use the comment box (grammar fixes are also welcome)