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.

链接地址: http://www.djcxy.com/p/51260.html

上一篇: C ++头文件的C ++实现?

下一篇: 变量如何在lambda函数中工作?