Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

PyQt. Руководим памятью, собираем мусор

Anna | 15.06.2014 | нет комментариев
Давным давным-давно был язык С. И были в нем 2 функции руководящие памятью — malloc и free. Но это было слишком трудно.
Посмотрел на это Бьёрн Страуструп и решил что необходимо сделать все проще. И изобрел С . В дополнение к malloc/free там возникли new/delete, деструкторы, RAII, auto и shared указатели.
Посмотрел на это Гвидо ван Россум, и решил, что С тоже не довольно примитивен. Он решил идти иным путем и придумал Python, в котором даже malloc и free нет.
А тем временем норвежские троли сотворили на С GUI-библиотеку Qt, которая упрощает управление памятью для своих объектов за счет того, что сама их удаляет, когда посчитает необходимым.
Phil Thompson расстроился, что хорошей библиотеки Qt нет для восхитительного языка Python. И решил их объединить планом PyQt. Впрочем, как оказалось, если скрестить различные парадигмы управления памятью, непременно вылезут побочные результаты. Давайте посмотрим какие…

*Историческая честность и хронология принесена в жертву художественной составляющей введения.

Модель работы PyQt дозволено упрощенно представить дальнейшим образом: для всякого публичного класса С создается класс-обертка в Python. Программист работает с объектом-оберткой, а она вызывает способы «подлинного» C -объекта.
Все отлично, пока объект и обертка синхронно создаются и синхронно гибнут. Но эту синхронность дозволено нарушить. У меня получалось это сделать 3-я методами:

  • Python-обертка сделана, С объект- нет
  • Сборщик мусора Python удалил необходимый объект
  • Qt удалила объект. Python-обертка жива

Python-обертка сделана, С объект- нет

    from PyQt4.QtCore import QObject

    class MyObject(QObject):
        def __init__(self):
            self.field = 7

    obj = MyObject()
    print(obj.field)
    obj.setObjectName("New object")

>>> Traceback (most recent call last):
>>>   File "pyinit.py", line 9, in <module>
>>>     obj.setObjectName("New object")
>>> RuntimeError: '__init__' method of object's base class (MyObject) not called.

Данный и другие примеры дозволено посмотреть тут

В конструкторе MyObject мы не вызвали конструктор базового класса. При этом объект удачно создался, им дозволено пользоваться. Впрочем при первой попытке вызвать C -способ мы получим RuntimeError с объяснением, что мы сделали не верно.
Поправленный вариант:

    ...
    class MyObject(QObject):
        def __init__(self):
            QObject.__init__(self)
    ...

Сборщик мусора Python удалил необходимый объект

    from PyQt4.QtGui import QApplication, QLabel

    def createLabel():
        label = QLabel("Hello, world!")
        label.show()

    app = QApplication([])
    createLabel()

    app.exec_()

Если бы данный код был написан на C , позже app.exec_() мы бы получили окошко c «Hello, world!». Но, данный код ничего не покажет. Когда функция createLabel() завершила выполняться, в Python-коде огромнее не осталось ссылок на label, и рачительный сборщик мусора удалил Python-обертку. В свою очередь обертка удалила C -объект.

Поправленный вариант:

    from PyQt4.QtGui import QApplication, QLabel

    def createLabel():
        label = QLabel("Hello, world!")
        label.show()
        return label

    app = QApplication([])
    label = createLabel()

    app.exec_()

Сберегаем ссылки на все сделанные объекты, даже если не собираемся пользоваться этими ссылками.

Qt удалила объект. Python-обертка жива

Предыдущие 2 случая описаны в документации к PyQt/Pyside и достаточно банальны. Значительно больше трудные задачи появляются, когда Python-часть не знает о том, что библиотека Qt удалила C -объект.
Qt может удалить объект при удалении родительского объекта, закрытии окна, вызове deleteLater() ив некоторых других обстановках.
Позже удаления дозволено трудиться с способами обертки, написанными на чистом Python, а попытка доступа к C -части вызывает RuntimeError либо падение приложения.

Начнем с дюже простого метода выстрелить себе в ногу:

    from PyQt4.QtCore import QTimer
    from PyQt4.QtGui import QApplication, QWidget

    app = QApplication([])

    widget = QWidget()
    widget.setWindowTitle("Dead widget")
    widget.deleteLater()

    QTimer.singleShot(0, app.quit)  # Делаем так, Дабы приложение завершилось сразу позже старта
    app.exec_()  #  Запускаем приложение, Дабы оно исполнило deleteLater()

    print(widget.windowTitle())

>>> Traceback (most recent call last):
>>>   File "1_basic.py", line 20, in <module>
>>>     print(widget.windowTitle())
>>> RuntimeError: wrapped C/C   object of type QWidget has been deleted

Создаем QWidget, умоляем Qt его удалить. Во время app.exec_() объект будет удален. Обертка об этом не знает, и при попытке вызвать windowTitle() кинет исключение либо вызовет падение.
Разумеется, если программист вызвал deleteLater() а потом использует объект, то он сам и повинен. Впрочем в настоящем коде Зачастую случается больше трудный сценарий:

  1. Создаем объект
  2. Подключаем внешние сигналы к слотам объекта
  3. Qt удаляет объект. Скажем, при закрытии окна
  4. Слот удаленного объекта вызывается таймером либо сигналом из внешнего мира
  5. Приложение падает либо генерирует исключение
Длинный приближенный к жизни пример

    from PyQt4.QtCore import Qt, QTimer
    from PyQt4.QtGui import QApplication, QLabel, QLineEdit

    def onLineEditTextChanged():
        print('~~~~ Line edit text changed')

    def onLabelDestroyed():
        print('~~~~ C   label object destroyed')

    def changeLineEditText():
        print('~~~~ Changing line edit text')
        lineEdit.setText("New text")

    class Label(QLabel):
        def __init__(self):
            QLabel.__init__(self)
            self.setAttribute(Qt.WA_DeleteOnClose)
            self.destroyed.connect(onLabelDestroyed)

        def __del__(self):
            print('~~~~ Python label objВ качестве источника внешних сигналов применяется QLineEdit, а в качестве удаляемого объекта - Label.ect destroyed')

        def setText(self, text):
            print('~~~~ Changing label text')
            QLabel.setText(self, text)

        def close(self):
            print('~~~~ Closing label')
            QLabel.close(self)

    app = QApplication([])
    app.setQuitOnLastWindowClosed(False)

    label = Label()
    label.show()

    lineEdit = QLineEdit()
    lineEdit.textChanged.connect(onLineEditTextChanged)
    lineEdit.textChanged.connect(label.setText)

    QTimer.singleShot(1000, label.close)   # пользователь закрыл одно из окон
    QTimer.singleShot(2000, changeLineEditText)  # пользователь изменил текст в ином окне. Случилось исключение.
    QTimer.singleShot(3000, app.quit)

    app.exec_()

    print('~~~~ Application exited')

>>> ~~~~ Closing label
>>> ~~~~ C   label object destroyed
>>> ~~~~ Changing line edit text
>>> ~~~~ Line edit text changed
>>> ~~~~ Changing label text
>>> Traceback (most recent call last):
>>>   File "2_reallife.py", line 33, in setText
>>>     QLabel.setText(self, text)
>>> RuntimeError: wrapped C/C   object of type Label has been deleted
>>> ~~~~ Application exited
>>> ~~~~ Python label object destroyed

Label подключен к сигналу textChanged от QLineEdit. Через 1 секунду позже запуска label закрывается и удаляется. Программисту и пользователю он огромнее не необходим. Впрочем через 2 секунды удаленный label получает сигнал. В консоль сыплется исключение либо приложение невзначай падает.

Когда слоты не отключаются механически

В С -приложениях при удалении объекта отключаются все его слоты, следственно задач не появляется. Впрочем PyQt и PySide не неизменно могут «отключить» объект. Мне стало увлекательно разобраться, когда слоты не отключаются. В процессе экспериментов родился дальнейший тест:

Еще огромнее кода

    PYSIDE = False
    USE_SINGLESHOT = True

    if PYSIDE:
        from PySide.QtCore import Qt, QTimer
        from PySide.QtGui import QApplication, QLineEdit
    else:
        from PyQt4.QtCore import Qt, QTimer
        from PyQt4.QtGui import QApplication, QLineEdit

    def onLineEditDestroyed():
        print('~~~~ C   lineEdit object destroyed')

    def onSelectionChanged():
        print('~~~~ Pure C   method selectAll() called')

    class LineEdit(QLineEdit):
        def __init__(self):
            QLineEdit.__init__(self)
            self.setText("foo bar")

            self.destroyed.connect(onLineEditDestroyed)
            #self.selectionChanged.connect(onSelectionChanged)

        def __del__(self):
            print('~~~~ Python lineEdit object destroyed')

        def clear(self):
            """Overridden Qt method
            """
            print('~~~~ Overridden method clear() called')
            QLineEdit.clear(self)

        def purePythonMethod(self):
            """Pure python method.
            Does not override any C   methods
            """
            print('~~~~ Pure Python method called')
            self.windowTitle()  # generate exception

    app = QApplication([])
    app.setQuitOnLastWindowClosed(False)

    lineEdit = LineEdit()
    lineEdit.deleteLater()

    if USE_SINGLESHOT:
        #QTimer.singleShot(1000, lineEdit.clear)
        #QTimer.singleShot(1000, lineEdit.purePythonMethod)
        QTimer.singleShot(1000, lineEdit.selectAll)  # pure C   method
    else:
        timer = QTimer(None)
        timer.setSingleShot(True)
        timer.setInterval(1000)
        timer.start()

        #timer.timeout.connect(lineEdit.clear)
        #timer.timeout.connect(lineEdit.purePythonMethod)
        timer.timeout.connect(lineEdit.selectAll)  # pure C   method

    QTimer.singleShot(2000, app.quit)

    app.exec_()

    print('~~~~ Application exited')

Как выяснилось, итог зависит от того, какие слоты удаленного объекта были подключены к сигналам. Поведение слегка отличается в PyQt и PySide.

Тип слота PyQt PySide
способ С -объекта слот отключается слот отключается
способ либо функция на чистом Python падение слот отключается
способ С -объекта перегруженный Python-оберткой падение падение

Решение

С удалением C -объектов исключительно трудно бороться. Проявляется она изредка не скоро, и вовсе не очевидно. Некоторые советы:

  • Если собираетесь удалить объект, у которого есть Python-слоты, вручную отключайте объект от сигналов извне
  • Дабы отследить момент удаления объекта дозволено применять сигнал QObject.destroyed, но не способ__del__ Python-обертки
  • Не используйте QTimer.singleShot для объектов, которые могут быть удалены. Такой таймер немыслимо остановить.

Если есть серебрянная пуля, буду рад прочитать про нее в комментариях.

Завершение

Верю никто не сделал итог, что следует опасаться PyQt/PiSide? На практике задачи случаются не Зачастую. У всякого инструмента есть крепкие и слабые стороны, которые необходимо знать.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB