How do variable work inside a lambda function?
I am trying to connect 9 different buttons to one handler using a lambda function and some PyQt5 QPushButtons in python 3.6. If I assign them individually using ints all works fine. However if I try to use a list and a loop they are all assigned to a button number of 10. I can't understand why, since I would have thought that my assignment was to the integer value and that my variable was out of scope. Clearly, there is something I don't understand going on here. Can anyone explain the behavior of this code?
self.buttonList = [ self.sq1Button,
self.sq2Button,
self.sq3Button,
self.sq4Button,
self.sq5Button,
self.sq6Button,
self.sq7Button,
self.sq8Button,
self.sq9Button]
buttonNumber = 1
for button in self.buttonList:
button.clicked.connect(lambda: self.squareButtonHandler(buttonNumber))
buttonNumber += 1
When python executes a function, it creates a namespace to hold local variables. The lambda in
button.clicked.connect(lambda: self.squareButtonHandler(buttonNumber))
is an inner function that contains a reference to buttonNumber
in the outer scope. When you pass that lambda to button.clicked.connect
, python has to remember that reference somehow. It does that by adding the context of the outer scope to the function object that it creates and passes to connect
. The function objects for all of the buttons you connected referene the same outer context and that means they will all see whatever is in buttonNumber
when the function exits.
Here is a running example showing your problem
def buttonHandler(num):
print('button', num)
def try_lambda():
handlers = []
for num in range(5):
handlers.append(lambda: buttonHandler(num))
return handlers
print("test 1")
for handler in try_lambda():
handler()
It produces
test 1
button 4
button 4
button 4
button 4
button 4
Yep, that's the problem. Lets take a look at the function objects we created by looking at the function object's closure
print("test 2")
for handler in try_lambda():
handler()
print(handler, handler.__closure__)
It shows
test 2
button 4
<function try_lambda.<locals>.<lambda> at 0x7f66e34a9d08> (<cell at 0x7f66e49fb3a8: int object at 0xa68aa0>,)
button 4
<function try_lambda.<locals>.<lambda> at 0x7f66e34a9d90> (<cell at 0x7f66e49fb3a8: int object at 0xa68aa0>,)
button 4
<function try_lambda.<locals>.<lambda> at 0x7f66e34a9e18> (<cell at 0x7f66e49fb3a8: int object at 0xa68aa0>,)
button 4
<function try_lambda.<locals>.<lambda> at 0x7f66e34a9ea0> (<cell at 0x7f66e49fb3a8: int object at 0xa68aa0>,)
button 4
<function try_lambda.<locals>.<lambda> at 0x7f66e349a048> (<cell at 0x7f66e49fb3a8: int object at 0xa68aa0>,)
Interesting. We got 4 different function objects (0x7f66e34a9d08, etc...) but a single cell holding the variable we want at 0x7f66e49fb3a8. That's why they all see the same number - they all use the same saved cell from the outer function's local variables.
In your case, partial
is a better option. It creates a function using a variable's current value and works likes you want.
import functools
def try_partial():
handlers = []
for num in range(5):
handlers.append(functools.partial(buttonHandler, num))
return handlers
print("test 3")
for handler in try_partial():
handler()
It produces
test 3
button 0
button 1
button 2
button 3
button 4
I had the same problem once and this helped me. What you basically need to do is move the click handler to a separate function and call the function with the buttonNumber
from inside the loop. This is probably due to how closures work and/or because it needs a new buttonNumber
every time the loop runs. I still don't understand the exact reason so if anyone does, please comment/edit.
上一篇: C ++头文件的C ++实现?
下一篇: 变量如何在lambda函数中工作?