Saving the current value of a variable for later use in a local scope

Question:

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.

Asked By: Jellby

||

Answers:

Closures capture the variable name, not the variable value. Since the value of the variable changes with each iteration then when the function is finally used it will reference the current value of item. To get around this you need to create your function in a new scope (another function).

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

However, you’re only really providing arguments to a function that already exists. As such you can use functools.partial instead.

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

Specific Qt problem

The reason why your Qt code doesn’t work is because the default argument you have provided is being overwritten. This is one reason why you shouldn’t use lambdas with default arguments to create closures (as mentioned here). If you look at the signature of clicked (which is what you are connecting your function to), you’ll notice that it takes a bool argument. This is why you are getting False. If you provide a no-args function then this boolean value is discarded by Qt. Use the two methods above to prevent this boolean argument being passed to your function.

Answered By: Dunes

Dunes has already given you a good answer in terms of what code you can use, so I won’t restate it, but I do want to explain why you need to do that.

The reasoning behind the fix you were given in the comments is the following: normally, Python saves names, not values. So when you write a function

lambda: print(item)

and invoke it later, that function will print whatever happens to be associated with (“bound to”) the name item at the time it is invoked. This should make it explicitly clear what’s going on:

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

However, default values of arguments are an exception. When you write

lambda item=item: print(item)

Python saves whatever value is bound to the name item at the time the function is declared, and uses that as the default if you don’t give it an argument when you invoke the function.

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

This can be used as a handy way to save a value from the current scope for later in the program. But here’s the catch: it’s a default value. It only gets used if you don’t give the function an argument. Continuing the previous code sample:

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

You get the idea.

It so happens that the PyQt slot you’re connecting to, namely QAbstractButton.clicked(), does take an argument: a boolean indicating whether the button is currently “checked” or not. (In Qt, buttons are a toggle control, with a binary state attached. What you think of as a standard button is just a Qt button that never sets its toggle state.) So Qt is calling your print_text method with one argument, False, reflecting the state of the button. One “proper” solution is to insert an explicit argument reflecting that:

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

Of course it’s probably better for code clarity that you use a partial object as Dunes suggested. That way you get to save your value in a way that can’t be overridden by an argument passed to the function.

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

Another option is to create your own object that stores the value, and pass a method of that object to connect(). You could define the print_text function in this object itself:

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

...

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

or you could make the object callable and implement print_text in its __call__ method, which makes the object itself act like a function:

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

...

button.clicked.connect(MyPartial(value))

or you could just give the object a reference to the thing that has the actual print_text() method, if that suits your program better.

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

or so on. All these options are variations on what the builtin functools.partial does.

Answered By: David Z
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.