Skip to content

Instantly share code, notes, and snippets.

@alvesjnr
Last active November 10, 2023 01:14
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alvesjnr/5006195 to your computer and use it in GitHub Desktop.
Save alvesjnr/5006195 to your computer and use it in GitHub Desktop.
I got trapped by a lambda function.

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.

The Problem

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).

The Reason

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

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)

def p(v):
print v
class A(object):
def __init__(self, name, command=lambda:None):
self.name = name
self.comman = command
def click(self):
self.comman()
def __repr__(self):
return "< %s:%s >" % (self.__class__.__name__, self.name)
k = []
for i in ('a', 'b', 'c'):
k.append( A(i, command=lambda:p(i)) )
for a in k:
a.click()
"""
will print:
c
c
c
"""
#solution
k = []
for i in ('a', 'b', 'c'):
k.append( A(i, command=lambda arg=i:p(arg)) )
for a in k:
a.click()
"""
will print:
a
b
c
"""
@scj7t4
Copy link

scj7t4 commented Feb 21, 2013

Not a bug, just behavior you didn't expect. The value of b is not bound to the lambda functions you make in the first list comprehension (or rather, the b is used by reference instead of by value) Your solution, I suspect fixes this because I a default value cannot remain as a reference.

Copy link

ghost commented Mar 20, 2017

This behavior is non-intuitive at first but (kinda) makes sense when you think about what happens. Code inside a function is not executed until you call that function, in which case any names it refers to are looked up in the relevant namespaces. In this case (your original example) the name 'b'. By the time all the functions have been created, 'b' has the value of the last item in the loop (e.g. 'Delete'). When the different functions are called, they will look up 'b', which has that same value 'Delete'... NOT the value it had when you first created the function.

It is not specific to lambdas or list/generator comprehensions, by the way. It works with regular function definitions and for loops as well, e.g.

functions = []
for x in ["a", "b", "c"]:
    def p():
        print("x is", x)
    functions.append(p)
    
for f in functions:
    f()

---
('x is', 'c')
('x is', 'c')
('x is', 'c')

As you already saw, one solution is to pass a default argument to the lambda/function, which will cause the current value to be associated with that argument (rather than the name), and so it will be used at runtime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment