5

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)

Community
  • 1
  • 1
Qiao
  • 16,565
  • 29
  • 90
  • 117
  • It starts from here - http://stackoverflow.com/questions/6084331/pyqt-creating-buttons-from-dictionary/6084655#6084655 – Qiao May 28 '11 at 01:13

3 Answers3

4

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)) )
senderle
  • 145,869
  • 36
  • 209
  • 233
  • 3
    @Qiao, by the way -- some people might advocate the use of `_` as the name for a `throw_away` variable. I won't take a position on that, except to say that it is one recognized convention. – senderle May 28 '11 at 02:11
  • Ok, got it. But there is on more little question that bothers me - who (and when) adds argument to this lambda. It is someone in PyQt. But it is other question. – Qiao May 28 '11 at 02:24
  • 1
    @Qiao, I don't know PyQt very well, but it's common for GUI libraries, when an event triggers a callback, to pass that event to the callback. My guess is that something like that is happening. Why it shows up as 'False' when converted to a string is a mystery to me. – senderle May 28 '11 at 04:22
  • @Qiao, see [pedrotech's answer](http://stackoverflow.com/questions/6159021/lambda-i-i-fooi-in-for-loop-not-working/6162445#6162445) for a fuller explanation of what is happening. – senderle May 28 '11 at 22:23
3

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)) )
pedrotech
  • 1,359
  • 10
  • 19
  • Ah, thanks & +1 for the link -- I did a halfhearted documentation search but didn't find what I was looking for. – senderle May 28 '11 at 22:18
-1

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.

Karl Knechtel
  • 62,466
  • 11
  • 102
  • 153
  • It's unnecessarily confusing to use `lambda` with `partial` like that. The main point of using `partial` is to avoid such indirection. The OP's intention is to update a label with some parameterised text, so the code should be clarified to do just that: `btn.clicked.connect( partial(label.setText, f"button {i}"))`. With your approach, the benefits of `partial` aren't fully exploited, since the code still exposes some irrelevant implementation details. (The `clicked` argument in your example is strictly redundant; leaving it out will not raise a type-error). – ekhumoro Aug 21 '22 at 10:12
  • We cannot directly use `partial(label.setText, f"button {i}")`, because that takes no arguments, and QT will give it one (the `clicked` boolean). IOW, the same problem I'm pointing out at the end. The indirection is to introduce a separate parameter which can then be bound. However, I agree it would be clearer the other way around: make a partial for the `setText` parameter, and then a `lambda` to accept and ignore `clicked`. I'll fix that. – Karl Knechtel Aug 21 '22 at 21:38
  • @ekhumoro "The clicked argument in your example is strictly redundant; leaving it out will not raise a type-error" How do you figure that was the case? The `lambda` would then have accepted one argument, then bound by the `partial`, leaving no free arguments; and the framework would pass in the boolean. Yes? – Karl Knechtel Aug 21 '22 at 21:44
  • Everything I said is correct and works exactly as stated. I know this, because I tested it, and I have many years experience of writing pyqt code (see [my profile](https://stackoverflow.com/users/984421/ekhumoro?tab=profile)). Your assumptions about how partial works in pyqt are wrong, because you seem unaware that [it will simply throw away unused arguments](https://www.riverbankcomputing.com/static/Docs/PyQt4/old_style_signals_slots.html#pyqt4-slots-and-qt-slots) when calling slots via a signal connection. Please test things properly rather than relying on your own intuitions. – ekhumoro Aug 22 '22 at 00:02
  • Please write your own answer, then. the other existing ones are similarly fraught, trying to accommodate the `clicked` argument. – Karl Knechtel Aug 22 '22 at 00:04
  • This is an old question on a topic which comes up very frequently in the pyqt/pyside tags. I have already written several answers on various related aspects of it, so I see no value in adding yet another one. This question should probably be closed as a duplicate. I only added a comment because you said had no experience of pyqt, and I thought it would be helpful to suggest some constructive corrections. There's nothing wrong with the other answers. It's a standard idiom in pyqt to use `lambda` like that, although many people also prefer using `partial` in the way I did above. – ekhumoro Aug 22 '22 at 00:21
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/247435/discussion-between-karl-knechtel-and-ekhumoro). – Karl Knechtel Aug 22 '22 at 00:21