101

Considering the following code snippet:

# directorys == {'login': <object at ...>, 'home': <object at ...>}
for d in directorys:
    self.command["cd " + d] = (lambda : self.root.change_directory(d))

I expect to create a dictionary of two function as following :

# Expected :
self.command == {
    "cd login": lambda: self.root.change_directory("login"),
    "cd home": lambda: self.root.change_directory("home")
}

but it looks like the two lambda function generated are exactly the same :

# Result :
self.command == {
    "cd login": lambda: self.root.change_directory("login"),
    "cd home": lambda: self.root.change_directory("login")   # <- Why login ?
}

I really don't understand why. Do you have any suggestions ?

martineau
  • 119,623
  • 25
  • 170
  • 301
FunkySayu
  • 7,641
  • 10
  • 38
  • 61

4 Answers4

122

You need to bind d for each function created. One way to do that is to pass it as a parameter with a default value:

lambda d=d: self.root.change_directory(d)

Now the d inside the function uses the parameter, even though it has the same name, and the default value for that is evaluated when the function is created. To help you see this:

lambda bound_d=d: self.root.change_directory(bound_d)

Remember how default values work, such as for mutable objects like lists and dicts, because you are binding an object.

This idiom of parameters with default values is common enough, but may fail if you introspect function parameters and determine what to do based on their presence. You can avoid the parameter with another closure:

(lambda d=d: lambda: self.root.change_directory(d))()
# or
(lambda d: lambda: self.root.change_directory(d))(d)
NULL
  • 2,549
  • 1
  • 15
  • 17
  • 5
    I did not realize default values were allowed in lambdas. Passing a lambda with a default argument as a keyword argument just looks weird: `command = lambda path = path: selected(path)`. – ArtOfWarfare Jan 15 '16 at 14:28
  • Yes thats correct, but what can I do, if the signature of function is declared, imagine the case of a bind or trace function of tkinter. – Nikolai Ehrhardt Sep 22 '21 at 16:41
33

This is due to the point at which d is being bound. The lambda functions all point at the variable d rather than the current value of it, so when you update d in the next iteration, this update is seen across all your functions.

For a simpler example:

funcs = []
for x in [1,2,3]:
  funcs.append(lambda: x)

for f in funcs:
  print f()

# output:
3
3
3

You can get around this by adding an additional function, like so:

def makeFunc(x):
  return lambda: x

funcs = []
for x in [1,2,3]:
  funcs.append(makeFunc(x))

for f in funcs:
  print f()

# output:
1
2
3

You can also fix the scoping inside the lambda expression

lambda bound_x=x: bound_x

However in general this is not good practice as you have changed the signature of your function.

robbie_c
  • 2,428
  • 1
  • 19
  • 28
  • Is this same problem that occurs with defining closures within for-loops using ```def```, that in Python for-loops don't get their own scopes? – bob Apr 07 '20 at 02:35
  • @robbie_c An interesting behavior to support your first example: if you `del x` after the loop where you create and append the various functions, calling `f()` will throw a NameError complaining that `x` does not exist. – Guimoute Jan 03 '23 at 21:15
16

Alternatively, instead of lambda, you can use functools.partial which, in my opinion, has a cleaner syntax.

Instead of:

for d in directorys:
    self.command["cd " + d] = (lambda d=d: self.root.change_directory(d))

it will be:

for d in directorys:
    self.command["cd " + d] = partial(self.root.change_directory, d)

Or, here is another simple example:

numbers = [1, 2, 3]

lambdas = [lambda: print(number) 
           for number in numbers]
lambdas_with_binding = [lambda number=number: print(number) 
                        for number in numbers]
partials = [partial(print, number) 
            for number in numbers]

for function in lambdas:
    function()
# 3 3 3
for function in lambdas_with_binding:
    function()
# 1 2 3
for function in partials:
    function()
# 1 2 3
Georgy
  • 12,464
  • 7
  • 65
  • 73
  • 3
    This is a clearly better approach and I make sure to recommend it when the opportunity arises. The `lambda x=x` trick is not just unintuitive, it exploits the same behaviour that is [a classic gotcha in another context](https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument). Binding arguments using `functools.partial` is explicit, and also [eta-reduced](https://en.wikipedia.org/wiki/Lambda_calculus#%CE%B7-reduction) (which makes it more DRY). Oh, also that default parameter could be overridden, but shouldn't be - a potential pitfall. – Karl Knechtel Jun 15 '20 at 06:51
3

I met the same problem. The selected solution helped me a lot, but I consider necessary to add a precision to make functional the code of the question: define the lambda function outside of the loop. By the way, default value is not necessary.

foo = lambda d: lambda : self.root.change_directory(d)
for d in directorys:
    self.command["cd " + d] = (foo(d))
  • Cleanest and best answer in my opinion. It reminds me of all that stuff in lambda calculus, lambda x. lambda y. ... – Jonathan Mugan May 05 '22 at 17:40
  • @JonathanMugan How is it cleaner than `self.command["cd " + d] = lambda d=d: self.root.change_directory(d)`? – Guimoute Jan 03 '23 at 21:13