Search code examples
pythonpyqt4signals-slots

Connecting slots and signals in PyQt4 in a loop


Im trying to build a calculator with PyQt4 and connecting the 'clicked()' signals from the buttons doesn't work as expected. Im creating my buttons for the numbers inside a for loop where i try to connect them afterwards.

def __init__(self):
    for i in range(0,10):
        self._numberButtons += [QPushButton(str(i), self)]
        self.connect(self._numberButtons[i], SIGNAL('clicked()'), lambda : self._number(i))

def _number(self, x):
    print(x)

When I click on the buttons all of them print out '9'. Why is that so and how can i fix this?


Solution

  • This is just, how scoping, name lookup and closures are defined in Python.

    Python only introduces new bindings in namespace through assignment and through parameter lists of functions. i is therefore not actually defined in the namespace of the lambda, but in the namespace of __init__(). The name lookup for i in the lambda consequently ends up in the namespace of __init__(), where i is eventually bound to 9. This is called "closure".

    You can work around these admittedly not really intuitive (but well-defined) semantics by passing i as a keyword argument with default value. As said, names in parameter lists introduce new bindings in the local namespace, so i inside the lambda then becomes independent from i in .__init__():

    self._numberButtons[i].clicked.connect(lambda checked, i=i: self._number(i))
    

    UPDATE: clicked has a default checked argument that would override the value of i, so it must be added to the argument list before the keyword value.

    A more readable, less magic alternative is functools.partial:

    self._numberButtons[i].clicked.connect(partial(self._number, i))
    

    I'm using new-style signal and slot syntax here simply for convenience, old style syntax works just the same.