Monday, September 8, 2014

Another helping of Lambda Stew

Back a few days I posted about methods of connecting the clicked() signal of a button to a slot, when there are a lot of otherwise-anonymous buttons all handled from the same slot.

I am never shy to admit being wrong, and there was something wrong with that code, although it did work. I will now admit to being wrong, wrong, and wrong.

Let's make the problem more general. Say you are using PyQt to build a software simulation of an old-time jukebox.

Part of your UI is a set of 26 pushbuttons, visible in the image as numbers 1-26, as well as four others, A-B-C-D. When any of the latter four are clicked, all that happens (besides a juicy ka-chunk sound effect) is storing its letter. When any of the 26 numbered buttons are pressed, the whole simulation is set in motion to load and play selection B-13 or whatever.

So you have 30 QPushbuttons in two lists, and they can be handled by just two slots, one for the four letter buttons and one for the 26 numbered ones. It would be stupid to write 30 slot methods. You want to connect the clicked() signal of each button to one of two slot methods, and somehow the slot has to know which button was clicked.

Wrong method #1

This is what I did in PPQT V1. Although it is tricky and it worked, it is quite wrong and nobody should use it.

    for j in range(number_of_buttons):
        self.connect(self.button_list[i], SIGNAL("clicked()"),
                                lambda b=i : self.button_click(b) )

Note this is using the "old" signal API. It says, connect the variant of clicked that has no parameter to the anonymous function lambda b=i : self.button_click(b). That creates for each button an anonymous function with the signature def anon(b=<a button number>). Python's rule for default arguments is that they are evaluated when the function is compiled; hence, the button number was baked into the argument list as a default value. Because the clicked() signal passed no argument, the default was used, and passed to the slot as its parameter.

This broke in PyQt5 because, using the new signal API, I didn't know how to ask for the no-parameter variant of the signal. The default version passes a boolean value, which overrode the clever default button-number value. But then I found out how to do it, resulting in...

Wrong method #2

This seemed to work when testing on my laptop, but it's wrong, don't use it.

        for j in range(number_of_buttons):
            self.button_list[j].clicked[()].connect(
                lambda b=j: self.button_click(b)
                )
 

When I tested it on my desktop, it failed with a message about no such overloaded signal. The difference? Laptop had PyQt5.2, the desktop had PyQt5.3. Between the two point releases, Phil had deliberately removed support the no-argument overload of clicked(). I got around that with...

Wrong method #3

Even trickier, and still wrong:

        for j in range(number_of_buttons):
            self.button_list[j].clicked.connect(
                lambda f, b=j: self.user_button_click(b)
                )

This connects each button to an anonymous function that has the signature def anon(f, b=j), where f receives the unneeded boolean value and ignores it. No second value is passed, so the default-argument button number can be passed.

All this was just so the slot could know which button was calling it. But there is already a way to know that! Back two-plus years ago when writing version 1, apparently I didn't know that, or thought it was not kosher to use it, or something. And now working on V2, I was just trying to make the old code work instead of rethinking it.

Correct method

The right way is this way:

        for j in range(number_of_buttons):
            self.user_buttons[j].clicked.connect( self.button_click )

Just connect the damn signal. Then, in the slot method,

        button = self.sender() # object generating the signal
        button_number = self.button_list.index(button)

The QObject.sender() method, in a slot, returns a reference to the object that generated the signal. That's the button. If it's important to know which button, Python will happily tell you its index in your list of buttons.

So slow to get smart, I am.

No comments: