Будет ли следующий код отключать все входящие или исходящие сигналы PyQt?

В моем приложении PyQt5 (Python 3.8) многие QWidget() создаются и уничтожаются динамически. Хотя на уничтожение каждого QWidget() обращаю пристальное внимание - наверняка что-то проскочило. С каждым циклом потребление оперативной памяти увеличивается.

1. Один лайнер, чтобы отключить их все

Я прочитал следующее сообщение в блоге об отключении pyqtSignal()s: https://www.sep.com/blog/prevent-signal-slot-memory-leaks-in-python/

Внизу сообщения в блоге упоминается следующая однострочная строка для отключения всех pyqtSignal() от/от (?) QObject():

for x in filter(lambda y: type(y) == pyqtBoundSignal and 0 < element.receivers(y), map(lambda z: getattr(element, z), dir(element))): x.disconnect()

Предположим, что element представляет экземпляр QObject(), интересно, уничтожит ли этот однострочный код все входящие или исходящие сигналы?

Я попытался переписать эту однострочную функцию в реальной функции:

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

def disconnect_signals_connected_to_qobject(qobj:QObject) -> None:
    '''
    Disconnect all signals that are connected to a slot in the given 'qobj'.
    '''
    for x in filter(
            lambda y: type(y) == pyqtBoundSignal and 0 < qobj.receivers(y),
            map(
                lambda z: getattr(qobj, z),
                dir(qobj)
            )
    ):
        x.disconnect()
    return

Если код уничтожает все входящие сигналы, то выбранное имя disconnect_signals_connected_to_qobject() правильное. В противном случае я должен переименовать функцию в disconnect_signals_emitted_from_qobject(). Какое правильное имя?

2. Действительно ли это отключает *все* pyqtSignals?

У меня все еще есть это щемящее чувство, что код забудет несколько разъединений. Некоторое время назад я нашел следующий фрагмент кода, в котором утверждается, что он полностью отключает сигнал:

def discon_sig(signal):
    '''
    Disconnect only breaks one connection at a time,
    so loop to be safe.
    '''
    while True:
        try:
            signal.disconnect()
        except TypeError:
            break
    return

Похоже, что однострочник из сообщения в блоге забыл тот факт, что вызов signal.disconnect() отключает только одно соединение за раз. Поэтому, возможно, мне следует переписать свою функцию следующим образом:

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

def disconnect_signals_connected_to_qobject(qobj:QObject) -> None:
    '''
    Disconnect all signals that are connected to a slot in the given 'qobj'.
    '''
    # Define inner function for a complete signal
    # disconnection
    def discon_sig(signal):
        while True:
            try:
                signal.disconnect()
            except TypeError:
                break
        return

    # Loop to find all signals that need
    # disconnection
    for x in filter(
            lambda y: type(y) == pyqtBoundSignal and 0 < qobj.receivers(y),
            map(
                lambda z: getattr(qobj, z),
                dir(qobj)
            )
    ):
        discon_sig(x)
    return

3. Достаточно ли отключения?

Я стал еще больше беспокоиться после прочтения этой переписки между пользователем и создателем PyQt Филом Томпсоном:

https://www.riverbankcomputing.com/pipermail/pyqt/2019-September/042180.html

Короче говоря, пользователь утверждает, что отключения pyqtSignal()s недостаточно для предотвращения утечек памяти. Согласно его экспериментам, крайне важно, чтобы каждый pyqtSignal() был привязан к украшенному слоту. Бросьте в несколько неукрашенных слотов, и вас ждет вечная гибель:

Кевин («пользователь» пишет Филу Томпсону)

Мое предположение заключалось в том, что использование памяти сигнальными соединениями не будет накапливаться (т. е. что использование памяти не будет зависеть от количества созданных экземпляров SignalObject()), что, по-видимому, и происходит с @pyqtSlot() оформленной версией. В create_slot_objects() создается экземпляр каждого SlotObject(), который соединяет сигналы, а затем должен быть немедленно собран мусор, что должно разъединить сигналы. В версии скрипта без декораторов @pyqtSlot() число, сообщаемое для Память после создания объектов слота, пропорционально количеству созданных экземпляров SlotObject(), поэтому я предположил, что произошла утечка памяти, но я может, конечно, продумывать вещи неправильно.

Я не смог найти четкого ответа от Фила, поэтому теперь я официально параноик по поводу всех без исключения pyqtSignal(), живущих в моем приложении.

4. Является ли отключение *всех* сигналов действительно правильным?

Я только что провел тест своего приложения. При просмотре виджетов моего QGridLayout() (в обратном порядке) я делаю следующее, чтобы очистить их:

disconnect_signals_connected_to_qobject(widg)
widg.setParent(None)
widg.deleteLater()
sip.delete(widg)

Несмотря на это, у меня была утечка памяти почти 100 MB за цикл, причем один цикл заключался в заполнении моей таблицы 1000 строками и повторной очистке таблицы.

Теперь я опускаю отключение pyqtSignal:

widg.setParent(None)
widg.deleteLater()
sip.delete(widg)

Утечка памяти снизилась примерно до 20~25 MB за цикл. Как это возможно? Может быть, неизбирательное отключение всех pyqtSignal препятствует надлежащей очистке задействованных QWidget()? Может быть, нужно тщательно решить, какие pyqtSignal оставить, а какие отключить? Означает ли это, что сообщение в блоге, процитированное в начале этого вопроса (см. https://www.sep.com/blog/prevent-signal-slot-memory-leaks-in-python/) на самом деле дает плохой совет?


person K.Mulier    schedule 29.01.2021    source источник
comment
вызов signal.disconnect() отключает только одно соединение за раз. AFAIK (как также говорится в документации), signal.disconnect([slot]) Если [слот] опущен, сигнал отключается от всего, к чему он подключен. Можете ли вы предоставить MRE?   -  person musicamante    schedule 29.01.2021
comment
Первый пример, показанный в разделе 2, представляет собой переписанный с искажениями код, предназначенный для другой цели. Похоже, что он основан на неправильном толковании этого ответа, который не содержит утверждений, на которые вы ссылаетесь. На самом деле, в нем четко указано, что цикл while необходим для безопасного отключения конкретного обработчика, который несколько раз подключался к одному и тому же сигналу. Однако в интересах ясности я немного изменил ответ, чтобы избежать дальнейших сомнений.   -  person ekhumoro    schedule 30.01.2021
comment
Я также хотел бы поддержать запрос musicamante на MRE. Существует много разных причин, по которым может произойти утечка памяти, поэтому без конкретного тестового примера однозначного ответа на самом деле не бывает.   -  person ekhumoro    schedule 30.01.2021
comment
Привет, @musicamante и @ekhumoro, я хотел бы дать MRE, но это большое приложение, и извлечь из него MRE очень сложно. Однако я только что получил уведомление от коллеги о том, что утечка памяти устранена. Применение QStyle() к меню — то, что мы привыкли делать с некоторыми настроенными QWidget(), — казалось, мешало правильной сборке мусора. Мы не знаем, почему именно — может быть, это пища для дополнительных исследований или другой вопрос. Спасибо @ekhumoro за разъяснение stackoverflow.com/ вопросы/21586643/   -  person K.Mulier    schedule 30.01.2021