Before discussing the differences between lambda/partial and some other issues, I will briefly cover some practical fixes for your code example.
There are two main problems. The first is a common issue with the binding of names in a for-loop, as discussed in this question: Lambda in a loop. The second is a PyQt-specific problem with default signal paramters, as discussed in this question: Unable to send signal from Button created in Python loop. Given this, your example can be fixed by connecting the signals like this:
bh.clicked.connect(lambda checked, n=n: self.printtext(n+1))
So this caches the current value of n
in a default argument, and also adds a checked
argument to stop n
being overwritten by the parameter emitted by the signal.
The example could also benefit from being re-written in a more idiomatic way which eliminates the nasty eval hack:
layout = QtWidgets.QVBoxLayout(self)
for n in range(1, 4):
button = QtWidgets.QPushButton(f'button {n}')
button.clicked.connect(lambda checked, n=n: self.printtext(n))
layout.addWidget(button)
setattr(self, f'button{n}', button)
or using a QButtonGroup:
self.buttonGroup = QtWidgets.QButtonGroup(self)
layout = QtWidgets.QVBoxLayout(self)
for n in range(1, 4):
button = QtWidgets.QPushButton(f'button {n}')
layout.addWidget(button)
self.buttonGroup.addButton(button, n)
setattr(self, f'button{n}', button)
self.buttonGroup.buttonClicked[int].connect(self.printtext)
(Note that button-groups can also be created in Qt Designer by selecting the buttons and then choosing "Assign to button group" in the context-menu).
As for the question of the differences between lambda and partial: the main one is that the former implicitly creates a closure over the local variables, whereas the latter explicitly stores the arguments passed to it internally.
The common "gotcha" with closures in python is that functions defined inside a loop don't capture the current value of the enclosed variables. So when the function is called later, it can only access the last seen values. There was a discussion about changing this behaviour fairly recently on the python-ideas mailing list, but it didn't seem to reach any firm conclusions. Still, it's possible some future version of python will remove this little wart. The issue is completely avoided by the partial
function because it creates a callable object which stores the arguments passed to it as read-only attributes. So the work-around of using default arguments in a lambda
to explicitly store the variables is effectively using that same approach.
There is one other point worth covering here: the differences between PyQt and PySide when connecting to signals with default parameters. It appears that PySide treats all slots as if they were decorated with a slot decorator; whereas PyQt treats undecorated slots differently. Here is an illustration of the differences:
def __init__(self):
...
self.button_1.clicked.connect(self.slot1)
self.button_2.clicked.connect(self.slot2)
self.button_3.clicked.connect(self.slot3)
def slot1(self, n=1): print(f'slot1: {n=!r}')
def slot2(self, *args, n=1): print(f'slot2: {args=!r}, {n=!r}')
def slot3(self, x, n=1): print(f'slot1: {x=!r}, {n!r}')
After clicking each button in turn, the following output is produced:
PyQt5 output:
slot1: n=False
slot2: args=(False,), n=1
slot3: x=False, n=1
PySide2 output:
slot1: n=False
slot2: args=(), n=1
TypeError: slot3() missing 1 required positional argument: 'x'
If the slots are decorated with @QtCore.pyqtSlot()
, the output from PyQt5 matches the PySide2 output shown above. So if you need a solution for lambda (or any other undecorated slot) that works the same for both PyQt and PySide, you should use this:
bh.clicked.connect(lambda *args, n=n: self.printtext(n))