lambda i=i: foo(i) in for loop not working

Question:

First read this. It is about lambda x=x: foo(x) catching x even in for loop.

This is a window with label and two buttons generated in for loop. When button is clicked, it name appears in label.

If we use usual lambda: label.setText("button -- " + str(i)), then the result is last i in the loop, no matter what button is pressed:
lambda:foo(i)
And this is right.

When we change to lambda i=i: label.setText("button -- " + str(i)) (snipet) and expect that now it will be everything ok, the result is:
lambda i=i:foo(i)]
False!

Where this False comes from?

import sys
from PyQt4.QtGui import *

class MainWindow(QWidget):
    def __init__(self, parent=None):
        QWidget.__init__(self, parent)

        vbox = QVBoxLayout(self)

        # label for action
        label = QLabel('')
        vbox.addWidget(label)

        # adding buttons
        for i in range (1, 3):
            btn = QPushButton(str(i))
            btn.clicked.connect( lambda i=i: label.setText("button " + str(i)) )
            vbox.addWidget(btn)

app = QApplication(sys.argv)
myapp = MainWindow()
myapp.show()
sys.exit(app.exec_())

Why this solution is not working as it should be? What this false means?

I know that you can make foo_factory, as in first link, but the question is what is wrong with lambda i=i: foo(i)

Asked By: Qiao

||

Answers:

I don’t have PyQt4 installed to test at this very instant, but it seems clear to me that when your lambda callback is called, it’s being given an argument. i is then equal to whatever the argument is, instead of the default value. Try this and tell me if it works (or if it at least changes the output):

btn.clicked.connect( lambda throw_away=0, i=i: label.setText("button " + str(i)) )
Answered By: senderle

Signal “clicked” passes a boolean argument to your connected lambda slot.
Documentation

What you are trying to accomplish is better done by this:

btn.clicked.connect( lambda clicked, i=i : label.setText("button " + str(i)) )
Answered By: pedrotech

Instead of binding via a default argument, binding via functools.partial makes these problems easier to debug.

The correct code (if I have understood the other answers correctly; I don’t have PyQT experience) should look like:

from functools import partial

# and then:
set_to_i = partial(label.setText, f"button {i}")
btn.clicked.connect(lambda clicked: set_to_i())

This way, we are binding the value using a tool explicitly made for the job, rather than exploiting what is usually considered a gotcha. Notably, if we had taken this approach initially, but overlooked the clicked argument (for example, directly writing btn.clicked.connect(set_to_i)), we would get a TypeError, rather than the default-bound i being overridden by what is supposed to be the clicked boolean parameter. Seeing that the function was given an extra positional parameter, would be a clue to check the documentation.

Answered By: Karl Knechtel
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.