【问题标题】:Saving the current value of a variable for later use in a local scope保存变量的当前值以供以后在本地范围内使用
【发布时间】:2016-05-01 12:57:14
【问题描述】:

我想在循环中创建函数,具体取决于循环变量(用于 PyQT),但这些函数并没有像我想要的那样“取消引用”循环变量。 (我不知道正确的术语,所以请原谅我的草率。)这是一个简单的例子:

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

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

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

我希望b 是一个函数列表,每个函数都打印a 的相应元素(在定义时,如果稍后修改a,它们不应更改)。正如链接中所建议的,func = lambda item=item: print(item) 修复了这个简单的案例,但我无法让我的 PyQT 案例使用相同的修复:

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_())

我想传递一个xml.etree 元素,但如果我使用 lambda 函数,我得到的只是False。通过显式创建每个按钮,一切正常。

【问题讨论】:

  • FWIW,b 中的函数 必须打印 a 的最后一个元素:它们实际上打印 item 的当前值调用,假设您在与创建它们相同的范围内调用它们。看看如果你把item = 42 放在for func in b: 循环之前会发生什么。
  • Jellby,我冒昧地清理了你的问题。请仔细检查并确保我的编辑没有太大变化。

标签: python pyqt4 name-binding


【解决方案1】:

闭包捕获变量名,而不是变量值。由于变量的值随着每次迭代而变化,因此当最终使用该函数时,它将引用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 来创建闭包的原因之一(如 here 所述)。如果您查看clicked 的签名(这是您将函数连接到的),您会注意到它需要一个bool 参数。这就是您收到False 的原因。如果你提供一个无参数函数,那么这个布尔值会被 Qt 丢弃。使用上面的两种方法来防止这个布尔参数被传递给你的函数。

【讨论】:

    【解决方案2】:

    Dunes 已经为您提供了a good answer 关于您可以使用的代码,所以我不会重述它,但我确实想解释一下您为什么需要这样做。

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

    lambda: print(item)
    

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

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

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

    lambda item=item: print(item)
    

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

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

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

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

    你明白了。

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

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

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

    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)
    

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

    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 所做的变化。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-11-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-05-30
      • 1970-01-01
      • 2011-09-05
      相关资源
      最近更新 更多