pyqt: правильный способ подключения нескольких сигналов к одной и той же функции в pyqt (QSignalMapper не применяется)


  1. Я подготовил много постов о том, как подключить несколько сигналов к одному и тому же обработчику событий в python и pyqt. Например, подключение нескольких кнопок или комбо-боксов к одной и той же функции.

  2. Многие примеры показывают, как это сделать с QSignalMapper, но это не применимо, когда сигнал несет параметр, как с combobox.currentIndexChanged

  3. Многие люди предполагают, что его можно сделать с помощью лямбды. Это чистое и красивое решение, я согласен, но никто упоминает, что lambda создает замыкание, которое содержит ссылку - таким образом, объект ссылки не может быть удален. Привет утечка памяти!

Доказательство:

from PyQt4 import QtGui, QtCore

class Widget(QtGui.QWidget):
    def __init__(self):
        super(Widget, self).__init__()

        # create and set the layout
        lay_main = QtGui.QHBoxLayout()
        self.setLayout(lay_main)

        # create two comboboxes and connect them to a single handler with lambda

        combobox = QtGui.QComboBox()
        combobox.addItems('Nol Adyn Dwa Tri'.split())
        combobox.currentIndexChanged.connect(lambda ind: self.on_selected('1', ind))
        lay_main.addWidget(combobox)

        combobox = QtGui.QComboBox()
        combobox.addItems('Nol Adyn Dwa Tri'.split())
        combobox.currentIndexChanged.connect(lambda ind: self.on_selected('2', ind))
        lay_main.addWidget(combobox)

    # let the handler show which combobox was selected with which value
    def on_selected(self, cb, index):
        print '! combobox ', cb, ' index ', index

    def __del__(self):
        print 'deleted'

if __name__ == '__main__':

    import sys
    app = QtGui.QApplication(sys.argv)

    wdg = Widget()
    wdg.show()

    wdg = None

    sys.exit(app.exec_())

Виджет не удаляется, хотя мы очищаем ссылку. Удалите соединение с lambda - оно будет удалено правильно.

Итак, возникает вопрос: Как правильно соединить несколько сигналов с параметрами в одном обработчике без утечки памяти?

2 3

2 ответа:

Это просто неверно, что объект не может быть удален, потому что сигнальное соединение содержит ссылку в замыкании. Qt автоматически удалит все сигнальные соединения, когда он удалит объект, который в свою очередь удалит ссылку на lambda на стороне python.

Но это означает, что вы не всегда можете полагаться только на Python для удаления объектов. У каждого объекта PyQt есть две части: Часть Qt C++ и часть Python wrapper. Обе части должны быть удалены - и иногда в определенном порядке (в зависимости от того, является ли Qt или Python в настоящее время владельцем объекта). В дополнение к этому, есть также капризы сборщика мусора Python, чтобы учитывать (особенно в течение короткого периода, когда интерпретатор выключается).

В любом случае, в вашем конкретном примере простое решение состоит в том, чтобы просто сделать:

    # wdg = None
    wdg.deleteLater()

Это планирование объекта для удаления, поэтому для его выполнения требуется цикл событий. В вашем примере это также будет автоматически выйти из приложения (так как объект является последним закрытым окном).

Чтобы более четко видеть, что происходит, вы также можете попробовать следующее:

    #wdg = None
    wdg.deleteLater()

    app.exec_()

    # Python part is still alive here...
    print(wdg)
    # but the Qt part has already gone
    print(wdg.objectName())

Вывод:

<__main__.Widget object at 0x7fa953688510>
Traceback (most recent call last):
  File "test.py", line 45, in <module>
    print(wdg.objectName())
RuntimeError: wrapped C/C++ object of type Widget has been deleted
deleted

EDIT:

Вот еще один пример отладки, который, надеюсь, сделает его еще более ясным:

    wdg = Widget()
    wdg.show()

    wdg.deleteLater()
    print 'wdg.deleteLater called'

    del wdg
    print 'del widget executed'

    wd2 = Widget()
    wd2.show()

    print 'starting event-loop'
    app.exec_()

Вывод:

$ python2 test.py
wdg.deleteLater called
del widget executed
starting event-loop
deleted

Во многих случаях параметр, передаваемый сигналом, может быть пойман другим способом, например, если для передающего объекта задано имя объекта, поэтому можно использовать QSignalMapper:

    self.signalMapper = QtCore.QSignalMapper(self)
    self.signalMapper.mapped[str].connect(myFunction)  

    self.combo.currentIndexChanged.connect(self.signalMapper.map)
    self.signalMapper.setMapping(self.combo, self.combo.objectName())

   def myFunction(self, identifier):
         combo = self.findChild(QtGui.QComboBox,identifier)
         index = combo.currentIndex()
         text = combo.currentText()
         data = combo.currentData()