суббота, 13 мая 2017 г.

О PyQt5, наследовании от классов Qt и утечках памяти

Cделаю отступление, и напомню, что Qt изначально писался на С++ и для С++, будучи ориентированный под его специфику. (В С++ контроля и освобождения памяти, есть только некоторые приёмы обеспечивающие очистку памяти. В Python есть сборщик мусора, достаточно простой, но удалением объектов и освобождением занимается именно он. Так же доступ к объектам происходит по указателям - они в создаются в куче.)

глядя на код наподобии:

class MyDialog(QDialog):

    def __init__(self, parent):
        super(MyDialog, self).__init__(parent)
        self.parent = parent
        self.initUI()

Тут мы можем получить ещё и кольцевые ссылки на уровне Python, но это другая история.
(К слову, если всё-таки требуется родительский элемент, стоит воспользоваться вызовом self.parent() .)

Да и просто во вроде бы безобидном классе:

class MyDialog(QDialog):
    def __init__(self, parent):
        super(MyDialog, self).__init__(parent)
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.initUI()

    def initUI(self):
        self.ok_button = QPushButton("OK", self)
        layout = QVBoxLayout(self)
        layout.addWidget(self.ok_button)
        self.show()

У меня были сомнения. Всё-таки стык между Python и С++. И на мой взгляд нужно быть хорошим знатоком одновременно Python, С++ и Qt, чтобы быть уверенным в происходящем.

Удивительно, но материалов в сети оказалось сравнительно мало:

http://stackoverflow.com/questions/37918012/pyqt-give-parent-when-creating-a-widget

Почти одно и то же, но на разных языках:
https://habrahabr.ru/post/210304/
http://enki-editor.org/2014/08/23/Pyqt_mem_mgmt.html

Проверить, остались ли в памяти виджеты (на сколько я понимаю, и С++ и Python сторон) можно,
добавив после вызова app.exec_() строку:
print('\n'.join(repr(w) for w in app.allWidgets()))

Один из способов избежать утечки памяти, переписать код так, чтобы ссылки хранились на стороне Python классов.

def openDialog(self):
    self.dialog = MyDialog()
    self.dialog.show()

Ещё один момент:

Если мы пишем свой класс, наследуясь от QWidget (или любых классов, унаследованных в свою очередь от QWidget), то безопаснее использовать атрибут WA_DeleteOnClose, если его можно будет установить.
Это обеспечит удалении класса на стороне С++, даже если обёртка на Python останется.
(В этом случае может возникнуть проблема, если остались не отсоединённые сигналы и слоты к С++ части класса.)

Для PyQt4:
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)

Пример:

class MyDialog(QDialog):
    def __init__(self, parent):
        super(MyDialog, self).__init__(parent)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)


Полный пример теста со stackoverflow, переписанный мной для PyQt5:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
from PyQt5.QtWidgets import (QApplication, QDialog,
                             QWidget, QPushButton, QVBoxLayout)
from PyQt5.QtCore import Qt


class MyDialog(QDialog):
    def __init__(self, parent):
        super(MyDialog, self).__init__(parent)
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.initUI()

    def initUI(self):
        self.ok_button = QPushButton("OK", self)
        layout = QVBoxLayout(self)
        layout.addWidget(self.ok_button)
        self.show()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    parent = QWidget()
    obj = MyDialog(parent)
    app.exec_()
    print('\n'.join(repr(w) for w in app.allWidgets()))


У меня показывает:
<PyQt5.QtWidgets.QDesktopWidget object at 0x000001F41CE94438>
<PyQt5.QtWidgets.QWidget object at 0x000001F41CE944C8>
<PyQt5.QtWidgets.QWidget object at 0x000001F41CE94288>

Что похоже на правду, поскольку parent и obj ещё в области видимости (scope) - она у них глобальная для скрипта (модуля).

Но obj остался только как обёртка - при попытке вызвать функцию из С++ сгенерировалась ошибка.

Если не писать self.setAttribute(Qt.WA_DeleteOnClose), то строк (а соответственно оставшихся в памяти объектов) будет больше, и они будут включать в себя QPushButton и всё остальное.

Несколько проверенных примеров просто с наследованием от QWidget или QMainWindow и созданием кнопок и прочих виджетов с указанием parent (а так же частичным хранением ссылок на них) показали утечку без WA_DeleteOnClose.

Итог:

Логичным выглядит стараться использовать self.setAttribute(Qt.WA_DeleteOnClose) в конструкторе при создании своих виджетов, главных окон и диалогов.
(В этом случае может возникнуть проблема, если остались неотсоединённые сигналы и слоты к С++ части класса.)

Комментариев нет:

Отправить комментарий