2

I'm connecting multiple signal/slots using a for loop in PyQt. The code is bellow:

# Connect Scan Callbacks
for button in ['phase', 'etalon', 'mirror', 'gain']:
    getattr(self.ui, '{}_scan_button' .format(button)).clicked.connect(
        lambda: self.scan_callback(button))

What I expect:

  • Connect button phase_scan_button clicked signal to the scan_callback slot and send the string phase as a parameter to the slot. The same for etalon, mirror and gain.

What I'm getting:

  • For some reason my functions is always passing the string gain as parameter for all the buttons. Not sure if I'm being stupid (likely) or it is a bug.

For reference, the slot method:

def scan_callback(self, scan):
    print(scan) # Here I always get 'gain'
    if self.scanner.isWorking:
        self.scanner.isWorking = False
        self.scan_thread.terminate()
        self.scan_thread.wait()
    else:
        self.scanner.isWorking = True
        self.scan_thread.start()
        getattr(self.ui, '{}_scan_button' .format(
            scan)).setText('Stop Scan')
        getattr(self, '_signal{}Scan' .format(scan)).emit()
Eduardo
  • 631
  • 1
  • 12
  • 25

3 Answers3

6

My preferred way of iterating over several widgets in pyqt is storing them as objects in lists.

myButtons = [self.ui.phase_scan_button, self.ui.etalon_scan_button,
             self.ui.mirror_scan_button, self.ui.gain_scan_button]
for button in myButtons:
    button.clicked.connect(lambda _, b=button: self.scan_callback(scan=b))

If you need the strings "phase", "etalon", "mirror" and "gain" separately, you can either store them in another list, or create a dictionary like

myButtons_dict = {"phase": self.ui.phase_scan_button,
                 "etalon": self.ui.etalon_scan_button,
                 "mirror": self.ui.mirror_scan_button,
                 "gain": self.ui.gain_scan_button}

for button in myButtons_dict:
    myButtons_dict[button].clicked.connect(lambda: _, b=button self.scan_callback(scan=b))

Note, how I use the lambda expression with solid variables that are then passed into the function self.scan_callback. This way, the value of button is stored for good.

offeltoffel
  • 2,691
  • 2
  • 21
  • 35
  • 2
    Please note GPhilo's answer aswell. He points out the problem of your approach, whereas my approach only helps you manage widgets and names! – offeltoffel Sep 19 '17 at 12:34
  • I did. Both are great answers, really hard to choose which one to accept – Eduardo Sep 19 '17 at 12:37
  • 1
    @offeltoffel I'm curious though... how is this different from the attempt the OP did? You're still defining a lambda that uses the value of `button`, which in theory should be evaluated at runtime. Why is this not the case? – GPhilo Sep 19 '17 at 12:39
  • GPhilo: You are completely right. I just tested it and found the same problem. It works though, if "button" is stored to another variable in the lambda expression (see updated version). Thanks for pointing out! – offeltoffel Sep 19 '17 at 12:46
  • 2
    @offeltoffel Nice trick in your last edit, although I'd have kept simply `self.scan_callback(b)` since specifying `scan=b` is redundant. @Eduardo, if you're wondering why this works with the seemingly minor change, [have a look at this amazing question and its answer.](https://stackoverflow.com/questions/1132941/least-astonishment-and-the-mutable-default-argument). – GPhilo Sep 19 '17 at 12:49
  • 2
    @GPhilo: I think what you call redundancy is more a matter of taste. Of course `self.scan_callback(b)` is the same as `self.scan_callback(scan=b)` but I personally like to keep track of which value is passed to which argument of the function. It get's nasty when there are 3 or more variables to pass. – offeltoffel Sep 19 '17 at 12:51
3

Your lambdas do not store the value of button when it is defined. The code describing the lambda function is parsed and compiled but not executed until you actually call the lambda. Whenever any of the buttons is clicked, the current value of variable button is used. At the end of the loop, button contains "gain" and this causes the behaviour you see.

Try this:

funcs = []
for button in ['phase', 'etalon', 'mirror', 'gain']:
    funcs.append( lambda : print(button))

for fn in funcs:
  fn()

The output is:

gain
gain
gain
gain

Extending the example, as a proof that the lambdas don't store the value of button note that if button stops existing, you'll have an error:

del button
for fn in funcs:
  fn()

which has output

funcs.append( lambda : print(button))
NameError: name 'button' is not defined
GPhilo
  • 18,519
  • 9
  • 63
  • 89
  • 2
    Thank you very much for your answer, it is very clear to me now. I, however, opted for @offeltoffel's answer because he proposed solutions for the issue. If I could, I would definitively accept both as correct answer. – Eduardo Sep 19 '17 at 12:36
1

As noted here : Connecting slots and signals in PyQt4 in a loop Using functools.partial is a nice workaround for this problem.

Have been struggling with same problem as OP for a day.

SWB
  • 11
  • 4