Using Lambda Expression to Connect Slots in Pyqt

Using lambda expression to connect slots in pyqt

The QPushButton.clicked signal emits an argument that indicates the state of the button. When you connect to your lambda slot, the optional argument you assign idx to is being overwritten by the state of the button.

Instead, make your connection as

button.clicked.connect(lambda state, x=idx: self.button_pushed(x))

This way the button state is ignored and the correct value is passed to your method.

Using lambda expression to connect slots and send multiple values in pyqt

Thank you, @musicamante, for the explanation. I am just adding it in the answer to mark it as solved if someone has similar issues.

As they mentioned, the value of the keyword argument of lambda is evaluated when it's created. So, creating a reference to sensorName.currentText() used the value of the current text at that time. But creating a reference to the ComboBox itself allowed for getting the value of the text in run-time.

Qt - Connect slot with argument using lambda

The issue is python's scoping rules & closures. You need to capture the group:

def connections(self):
for group in self.widgets:
self.widgets[group].clicked.connect(lambda g=group: self.openMenu(g))

def openMenu(self,group):
print(group)

PyQt, Python 3: Lambda slot assigning signal argument to a variable?

I don't think you can (or should want to) do this with lambda. See the answer to the possible duplicate (that I reference under your question) for what you can do.

One way to do something similar would be to simply define a single function which takes two arguments. The value, and some identifier you can use to look up the variable inside the function (eg an object reference or a string).

I might suggest:

object1.value_changed.connect(lambda val: store_var('var1', val)) 
object2.value_changed.connect(lambda val: store_var('var2', val))

How you implement store_var() of course is highly dependent on the rest of yoru code structure, whether you want to store the values in a dictionary or as global variables or instance attributes. But you shouldn't have any problem accessing the storage location from within your function if you were expecting to be able to access them from within a lambda expression.


Update

Based on your comment I assume the calls to the connect function are inside a method of the class that contains data. This you would define a method of that class as

def store_var(self, name, value):
setattr(self.data, name, value)

and then you would call

object1.value_changed.connect(lambda val: self.store_var('var1', val)) 

memory_profiler while using lambda expression to connect slots

The problem pointed out in this post does not have to do with PyQt5 but with the lambda functions since, like any function, it creates a scope and stores memory, to test what the OP points out I have created the following example:

from PyQt5 import QtCore, QtWidgets

class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)

self.counter = 0
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.on_timeout)
self.timer.start(1000)

self.button = QtWidgets.QPushButton("Press me")
self.setCentralWidget(self.button)

@QtCore.pyqtSlot()
def on_timeout(self):
self.counter += 1
if self.counter % 2 == 0:
self.connect()
else:
self.disconnect()

def connect(self):
self.button.setText("Connected")
self.button.clicked.connect(lambda checked: None)
# or
# self.button.clicked.connect(lambda checked, v=list(range(100000)): None)

def disconnect(self):
self.button.setText("Disconnected")
self.button.disconnect()

def main():
import sys

app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())

if __name__ == "__main__":
main()
  • self.button.clicked.connect(lambda checked: None)

Sample Image

  • self.button.clicked.connect(lambda checked, v=list(range(100000)): None)

Sample Image

As observed at the time of connection and disconnection, the memory consumed increases since the lambda maintains the information of v=list(range(100000)).


But in your code the closure stores only the variable "j" which is minimum:

getattr(self, i).clicked.connect(lambda pippo, j=i: self.printo(j))

To see how this variable affects I am going to eliminate the unnecessary code for the test (hangman005.py, etc) in addition to offering alternatives:

  • without connect:
class MainMenu(QtWidgets.QMainWindow):
def __init__(self):
super(MainMenu, self).__init__()
uic.loadUi("main_window2.ui", self)
self.hangman()

def hangman(self):
letters = "ABCDEFGHIJKLMNOPQRSTYVWXXYZ"
for i in letters:
pass

def printo(self, i):
print("oooooooooooooooooooooo :", i)

Sample Image

  • with lambda:
class MainMenu(QtWidgets.QMainWindow):
def __init__(self):
super(MainMenu, self).__init__()
uic.loadUi("main_window2.ui", self)
self.hangman()

def hangman(self):
letters = "ABCDEFGHIJKLMNOPQRSTYVWXXYZ"
for i in letters:
getattr(self, i).clicked.connect(lambda pippo, j=i: self.printo(j))

def printo(self, i):
print("oooooooooooooooooooooo :", i)

Sample Image

  • with functools.partial
class MainMenu(QtWidgets.QMainWindow):
def __init__(self):
super(MainMenu, self).__init__()
uic.loadUi("main_window2.ui", self)
self.hangman()

def hangman(self):
letters = "ABCDEFGHIJKLMNOPQRSTYVWXXYZ"
for i in letters:
getattr(self, i).clicked.connect(partial(self.printo, i))

def printo(self, i):
print("oooooooooooooooooooooo :", i)

Sample Image

  • once connect
class MainMenu(QtWidgets.QMainWindow):
def __init__(self):
super(MainMenu, self).__init__()
uic.loadUi("main_window2.ui", self)
self.hangman()

def hangman(self):
letters = "ABCDEFGHIJKLMNOPQRSTYVWXXYZ"
for i in letters:
getattr(self, i).setProperty("i", i)
getattr(self, i).clicked.connect(self.printo)

def printo(self):
i = self.sender().property("i")
print("oooooooooooooooooooooo :", i)

Sample Image

As we can see there is no significant difference between all the methods so in your case there is no memory leak or rather it is very small.

In conclusion: Every time you create a lambda method it has a closure (j = i in your case) so the OP recommends taking that into account, for example in your case len(letters) * size_of(i) are consumed which at Being small letters and i makes it negligible, but if you otherwise store a heavier object unnecessarily then it will cause problems

QT connect function using lambda

You can also use = to capture by value or use '&' capture by reference.

connect(btn,
&QPushButton::clicked,
this,
[=]
{
F1(str1);
};

Lambda slots for QPushButtons in a QTableWidget

It's not working because the i passed as a parameter to btnSlot is evaluated at the time btnSlot is called, NOT at the time the lambda is define. You need to freeze the value of i using a partial function, so:

from functools import partial

btn.clicked.connect(partial(self.btnSlot,i))

Another approach is to take advantage of the fact that default parameters are evaluated at the time function is defined, NOT at call time. So:

btn.clicked.connect(lambda checked,num=i: self.btnSlot(num))

How to use lambda function with the PyQt connect function when the widgets are instanciated in a loop?

The variables used inside a lambda are always evaluated at execution: widgetNumber will always be the last value in the for loop.

Also, signals can have arguments, and if you use a positional argument in the lambda, that argument will be the value of that signal when it's emitted: lambda widgetNumber: will result in the spinbox value or the state of the check box.

If you need "static" arguments, you need to use keyword arguments. Since the signal has arguments, there's also no need to call the getter again:

lambda value, widgetNumber=widgetNumber: self.argumentChanged(widgetNumber, value)

Remember that lambdas should be preferably used only when there is no other, simpler (clearer or cleaner) solution.

In this case, we can use the sender() function provided by Qt, which returns the object that emitted the signal.

In the following example I've simplified the whole loop, and used a custom property to set the number directly on the widget (note that also a basic attribute could be used, but properties are more consistent with Qt).

class MyWidget(QWidget):
def __init__(self, widgetList):
super(QWidget, self).__init__()
self.widgetList = widgetList
layout = QVBoxLayout()

for number, widgetType in enumerate(widgetList):

if widgetType == 'QCheckBox':
widget = QCheckBox()
widget.toggled.connect(self.argumentChanged)

elif widgetType == 'QSpinBox':
widget = QSpinBox()
widget.valueChanged.connect(self.argumentChanged)

widget.setProperty('widgetNumber', number)
layout.addWidget(widget)

self.setLayout(layout)

def argumentChanged(self, value):
widget = self.sender()
widgetNumber = widget.property('widgetNumber')
print("The widget number", widgetNumber,
"that is a", self.widgetList[widgetNumber],
"has been modified! New value:", value)

How to pass a QWidget's text through the parameter of Slot using lambda function

The next:

button_1.clicked.connect(lambda state, \
msg=self.mainWidget.layout().itemAt(0).widget().text(), \
srcIndex=0: self.showMessage(msg, srcIndex))

equals:

msg=self.mainWidget.layout().itemAt(0).widget().text()
srcIndex=0
button_1.clicked.connect(lambda state, msg=msg, srcIndex=srcIndex: self.showMessage(msg, srcIndex))

In reality you are passing the text when the window is being built and not the text that you have when the button is pressed.

The best thing is to create an intermediate function that gets the text:

    button_1.clicked.connect(lambda state, lineedit=lineEdit_1: self.foo(lineedit))
button_2.clicked.connect(lambda state, lineedit=lineEdit_2: self.foo(lineedit))

def foo(self, lineedit):
self.showMessage(lineedit.text())

def showMessage(self, message):
msgBox = QMessageBox()
msgBox.setWindowTitle("Message")
msgBox.setIcon(QMessageBox.Information)
msgBox.setText("\"" + str(message) + "\"")
msgBox.setStandardButtons(QMessageBox.Yes)
rtnVal = msgBox.exec()

Lifetime of object in lambda connected to pyqtSignal

The lambda in your example forms a closure. That is, it is a nested function that references objects available in its enclosing scope. Every function that creates a closure keeps a cell object for every item it needs to maintain a reference to.

In your example, the lambda creates a closure with references to the local self and model variables inside the scope of the __init__ method. If you keep a reference to the lambda somewhere, you can examine all the cell objects of its closure via its __closure__ attribute. In your example, it would display something like this:

>>> print(func.__closure__)
(<cell at 0x7f99c16c5138: MyModel object at 0x7f99bbbf0948>, <cell at 0x7f99c16c5168: MyClass object at 0x7f99bbb81390>)

If you deleted all other references to the MyModel and MyClass objects shown here, the ones kept by the cells would still remain. So when it comes to object cleanup, you should always explicitly disconnect all signals connected to functions that may form closures over the relevant objects.


Note that when it comes to signal/slot connections, PyQt treats wrapped C++ slots and Python instance methods differently. The reference counts of these types of callable are not increased when they are connected to signals, whereas lambdas, defined functions, partial objects and static methods are. This means that if all other references to the latter types of callable are deleted, any remaining signal connections will keep them alive. Disconnecting the signals will allow such connected callables to be garbage-collected, if necessary.

The one exception to the above is class methods. PyQt creates a special wrapper when creating connections to these, so if all other references to them are deleted, and the signal is emitted, an exception will be raised, like this:

TypeError: 'managedbuffer' object is not callable

The above should apply to PyQt5 and most versions of PyQt4 (4.3 and greater).



Related Topics



Leave a reply



Submit