I’m using Python 3.6.10 in a CentOS 7 environment. I am attempting to create a list of commands to execute based on a structured specification. It seems natural and pythonic to think of this as a list of lambdas. I build the lambda list by traversing the specification. To my surprise, when I execute the result, I find that every lambda is the same because it doesn’t capture its argument at the time the lambda is created. I think that’s a bug.
Here is sample code that illustrates the behavior:
specification = {
'labelOne': ['labelOne.one', 'labelOne.two', 'labelOne.three', 'labelOne.four', 'labelOne.five'],
'labelTwo': ['labelTwo.one', 'labelTwo.two', 'labelTwo.three', 'labelTwo.four', 'labelTwo.five'],
'labelThree': ['labelThree.one', 'labelThree.two', 'labelThree.three', 'labelThree.four', 'labelThree.five'],
'labelFour': ['labelFour.one', 'labelFour.two', 'labelFour.three', 'labelFour.four', 'labelFour.five'],
'labelFive': ['labelFive.one', 'labelFive.two', 'labelFive.three', 'labelFive.four', 'labelFive.five'],
}
lambdas = []
for label, labelStrings in specification.items():
for labelString in labelStrings:
lambdaString = f"""Label: "{label}" with labelString: "{labelString}""""
oneArgLambda = lambda someArg: print(someArg, lambdaString)
lambdas.append(oneArgLambda)
for each in lambdas:
each('Show: ')
I expected to see this:
Show: Label: "labelOne" with labelString: "labelOne.one"
Show: Label: "labelOne" with labelString: "labelOne.two"
Show: Label: "labelOne" with labelString: "labelOne.three"
Show: Label: "labelOne" with labelString: "labelOne.four"
Show: Label: "labelOne" with labelString: "labelOne.five"
Show: Label: "labelTwo" with labelString: "labelTwo.one"
Show: Label: "labelTwo" with labelString: "labelTwo.two"
Show: Label: "labelTwo" with labelString: "labelTwo.three"
Show: Label: "labelTwo" with labelString: "labelTwo.four"
Show: Label: "labelTwo" with labelString: "labelTwo.five"
Show: Label: "labelThree" with labelString: "labelThree.one"
Show: Label: "labelThree" with labelString: "labelThree.two"
Show: Label: "labelThree" with labelString: "labelThree.three"
Show: Label: "labelThree" with labelString: "labelThree.four"
Show: Label: "labelThree" with labelString: "labelThree.five"
Show: Label: "labelFour" with labelString: "labelFour.one"
Show: Label: "labelFour" with labelString: "labelFour.two"
Show: Label: "labelFour" with labelString: "labelFour.three"
Show: Label: "labelFour" with labelString: "labelFour.four"
Show: Label: "labelFour" with labelString: "labelFour.five"
Show: Label: "labelFive" with labelString: "labelFive.one"
Show: Label: "labelFive" with labelString: "labelFive.two"
Show: Label: "labelFive" with labelString: "labelFive.three"
Show: Label: "labelFive" with labelString: "labelFive.four"
Show: Label: "labelFive" with labelString: "labelFive.five"
Instead, I see this:
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
Show: Label: "labelFive" with labelString: "labelFive.five"
The argument binding of lambda is happening when the lambda is executed, rather than when the lambda is created. That is at least unexpected and I think arguably wrong.
I think lambda, as limited as it is, is supposed to create a CLOSURE — its entire purpose in life is to capture the state its arguments at the time it is created, so that they can be used later when the lambda is evaluated. That’s why it is called a "closure", because it closes over the value of its arguments at creation time.
What am I misunderstanding?
3
Answers
This is an alternative
As you said it create CLOSURE, and closure is using stated variable in upper scope of lambdaString, but the trick is that all of your lambdas uses the same ref to lambdaString and since you change it every time it remembers the last one. For instance:
You just need another closure to prevent this
Full code might be
You have some other comments and answers explaining what’s going on, and your
question is "interesting" is the sense that it forces the reader to puzzle over
the code and the various binding issues.
But if I saw your code from a colleague during a review, I would ask for a
rewrite – not because I would immediately know there was a bug, but because it
requires too much head-scratching over whether the bindings from the surrounding
(and changing) scope will behave exactly as hoped.
Instead, insist on stricter discipline in your programs, and thus lighten the
cognitive load on your reader (who is you most of the time). Specifically, move
the function creation to a truly isolated scope, and pass all of the varying
inputs to that function-creator. That approach is solid because it will either
work on the first try or fail entirely (if you neglect to pass all of the
needed arguments to the function-creator).
One way to do it: