1

I want to to create functions in a loop, depending on the loop variable (for use with PyQT), but the functions are not "dereferencing" the loop variable as I want. (I don't know the proper terminology, so forgive my sloppiness.) This is a simple example:

a = [1, 2, 3, 4]
b = []

for item in a:
    func = lambda: print(item)
    b.append(func)

print(a)
for func in b:
  func()

I'd want b to be a list of functions, each printing the corresponding element of a (at the time of definition, they shouldn't change if a is modified later). As suggested in the links, func = lambda item=item: print(item) fixes this simple case, but I can't make my PyQT case work with the same fix:

import sys
import xml.etree.ElementTree as ET
from PyQt4 import QtCore, QtGui

class Main(QtGui.QMainWindow):
    def __init__(self, parent=None):
        QtGui.QMainWindow.__init__(self, parent)
        self.tree = ET.fromstring('<root><a>1</a><a>2</a><a>3</a><a>4</a></root>')
        self.create_widgets()
        self.w = None

    def create_widgets(self):
        self.buttons = []

        # THIS DOESN'T WORK
        for value in self.tree.findall('a'):
            button = QtGui.QPushButton(value.text)
            button.clicked.connect(lambda x=value: self.print_text(x))
            self.buttons.append(button)

        # THIS DOES WORK:
        #values = []
        #for value in self.tree.findall('a'):
        #    values.append(value)
        #button = QtGui.QPushButton(values[0].text)
        #button.clicked.connect(lambda: self.print_text(values[0]))
        #self.buttons.append(button)
        #button = QtGui.QPushButton(values[1].text)
        #button.clicked.connect(lambda: self.print_text(values[1]))
        #self.buttons.append(button)
        #button = QtGui.QPushButton(values[2].text)
        #button.clicked.connect(lambda: self.print_text(values[2]))
        #self.buttons.append(button)
        #button = QtGui.QPushButton(values[3].text)
        #button.clicked.connect(lambda: self.print_text(values[3]))
        #self.buttons.append(button)

        box = QtGui.QHBoxLayout()
        for i in self.buttons:
            box.addWidget(i)
        central_widget = QtGui.QWidget()
        central_widget.setLayout(box)
        self.setCentralWidget(central_widget)

    def print_text(self, value):
        print(value)
        print(value.text)

if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    myapp = Main()
    myapp.show()
    sys.exit(app.exec_())

I want to pass an xml.etree element, but if I use the lambda function, what I get is only False. With explicit creation of each button, everything works fine.

4

2 回答 2

4

闭包捕获变量名称,而不是变量值。由于变量的值随着每次迭代而变化,因此当最终使用该函数时,它将引用 的当前值item。要解决这个问题,您需要在新范围(另一个函数)中创建函数。

def create_printer(item):
    def printer():
        print(item)
    return printer

a = [1, 2, 3, 4]
b = []

for item in a:
    func = create_printer(item)
    b.append(func)

print(a)
for func in b:
  func()

但是,您实际上只是为已经存在的函数提供参数。因此,您可以functools.partial改用。

from functools import partial

a = [1, 2, 3, 4]
b = []

for item in a:
    func = partial(print, item)
    b.append(func)

print(a)
for func in b:
  func()

具体Qt问题

您的 Qt 代码不起作用的原因是您提供的默认参数被覆盖了。这就是为什么您不应该使用带有默认参数的 lambdas 来创建闭包的原因之一(如此所述)。如果您查看clicked(这是您将函数连接到的)的签名,您会注意到它需要一个bool参数。这就是为什么你得到False. 如果你提供一个无参数函数,那么这个布尔值会被 Qt 丢弃。使用上面的两种方法来防止这个布尔参数被传递给你的函数。

于 2016-05-01T15:22:05.723 回答
3

Dunes 已经就可以使用的代码给出了一个很好的答案,所以我不会重述它,但我确实想解释为什么你需要这样做。

您在评论中给出的修复背后的原因如下:通常,Python 保存名称,而不是值。所以当你写一个函数

lambda: print(item)

并稍后调用它,该函数将打印在调用item它时与(“绑定”)名称相关联的任何内容。这应该清楚地说明发生了什么:

>>> item = 1
>>> func = lambda: print(item)
>>> func()
1
>>> item = 2
>>> func()
2

但是,参数的默认值是一个例外。当你写

lambda item=item: print(item)

itemPython 保存在函数声明时绑定到名称的任何值,如果在调用函数时不给它参数,则将其用作默认值。

>>> item = 1
>>> func = lambda item=item: print(item)
>>> func()
1
>>> item = 2
>>> func()
1

这可以用作从当前范围保存值以供程序稍后使用的便捷方式。但这里有一个问题:这是一个默认值。只有当你不给函数一个参数时,它才会被使用。继续前面的代码示例:

>>> func(3)
3
>>> item = 4
>>> func()
1
>>> func(item)
4

你明白了。

碰巧你正在连接的 PyQt 插槽,即QAbstractButton.clicked()确实接受了一个参数:一个布尔值,指示按钮当前是否被“选中”。(在 Qt 中,按钮是一个切换控件,附加了一个二进制状态。您认为的标准按钮只是一个从不设置其切换状态的 Qt 按钮。)所以 Qtprint_text使用一个参数调用您的方法False,反映按钮的状态。一个“正确”的解决方案是插入一个明确的参数,反映:

button.clicked.connect(lambda checked, x=value: self.print_text(x))

当然,使用Dunes 建议partial的对象可能会更好地提高代码的清晰度。这样你就可以以一种不能被传递给函数的参数覆盖的方式保存你的值。

button.clicked.connect(partial(self.print_text, value))

另一种选择是创建自己的存储值的对象,并将该对象的方法传递给connect(). 您可以print_text在此对象本身中定义函数:

class MyPartial:
    def __init__(self, value):
        self.value = value
    def print_text(self):
        # ...

...

button.clicked.connect(MyPartial(value).print_text)

或者您可以使对象可调用并print_text在其__call__方法中实现,这使对象本身就像一个函数:

class MyPartial:
    def __init__(self, value):
        self.value = value
    def __call__(self):
        # equivalent of print_text() goes here

...

button.clicked.connect(MyPartial(value))

或者您可以只为对象提供对具有实际方法的事物的引用print_text(),如果这更适合您的程序。

class MyPartial:
    def __init__(self, func, value):
        self.func = func
        self.value = value
    def __call__(self):
        self.func(self.value)

...

button.clicked.connect(MyPartial(self.print_text, value))

等等。所有这些选项都是内置functools.partial功能的变体。

于 2016-05-01T15:44:28.437 回答