Python 3.7: как избежать переполнения стека для этого рекурсивного подхода? - PullRequest
1 голос
/ 30 марта 2019

1.Ситуация

Я работаю над проектом на Python, и у меня довольно много функций следующего стиля:

from PyQt5.QtCore import *
import functools

...

    def myfunc(self, callback, callbackArg):
        '''
        This function hasn't finished its job when it hits the
        return statement. Provide a callback function and a
        callback argument, such that this function will call:
            callback(callbackArg)
        when it has finally finished its job.
        '''
        def start():
            myIterator = iter(self.myList)
            QTimer.singleShot(10, functools.partial(process_next, myIterator))
            return

        def process_next(itemIterator):
            try:
                item = next(itemIterator)
            except StopIteration:
                finish()

            # Do something

            QTimer.singleShot(10, functools.partial(process_next, myIterator))
            return

        def finish():
            callback(callbackArg)
            return

        start()
        return

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


2.Проблема

Но есть и обратная сторона.Этот подход создает большую нагрузку на стек (я думаю), потому что вы получили следующую цепочку:

start() -> process_next() -> process_next() -> process_next() -> ... -> finish()

Хотя я не совсем уверен в этом.Функция process_next() вызывает QTimer.singleShot(...) и затем завершается.Так что, может быть, этой длинной цепочки в стеке вообще не происходит?

Знаете ли вы, если такой подход создает риск переполнения стека?Есть ли другие потенциальные риски, которые я еще не обнаружил?


РЕДАКТИРОВАТЬ
Спасибо @ygramoel за разъяснения.Таким образом, на самом деле, следующая строка:

QTimer.singleShot(10, functools.partial(process_next, myIterator))

вызывает функцию process_next(myIterator), не выдвигая другой кадр стека.Поэтому я не рискую переполнением стека длинными списками.Отлично!

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

QTimer.singleShot(0, functools.partial(process_next, myIterator))

Однако каждый вызов QTimer.singleShot() запускает pyqtSignal().Запуск слишком большого количества из них за короткий промежуток времени расширяет основной поток до его пределов (помните: основной поток Python прослушивает входящие сигналы Pyqt).Основной поток обрабатывает записи очереди событий по очереди, вызывая соответствующие слоты.Поэтому, если программное обеспечение запускает слишком много событий в эту очередь, графический интерфейс может перестать отвечать на запросы.

Есть ли другой элегантный способ вызова process_next(myIterator) без каких-либо из следующих проблем:

  • Засорение очереди событий так, что графический интерфейс перестает отвечать на запросы.
  • Переполнение стека рекурсивными функциональными кадрами.

1 Ответ

1 голос
/ 30 марта 2019

Вы не включили код для item.foobar и self.foo.Предполагая, что эти вызовы не вызывают глубокую рекурсию, максимальная глубина стека во время выполнения этого кода не будет увеличиваться с длиной списка.

functools.partial не вызывает сразу функцию process_next.Он создает только функциональный объект, который можно вызвать позже.См. https://docs.python.org/3/library/functools.html

QTimer.singleShot также не вызывает немедленно функцию process_next.Он планирует, что функциональный объект, возвращенный из functools.partial, будет выполнен позже, после того, как текущий вызов к process_next вернется.

Вы можете легко убедиться в этом сами, поместив оператор print("enter") вначало process_next и оператор print("leave") непосредственно перед возвратом.

В случае рекурсии вы увидите:

enter
enter
enter
...
leave
leave
leave

и стек будет переполнен для очень длинных списков.

Если нет рекурсии, вы увидите:

enter
leave
enter
leave
enter
leave
...

и максимальная глубина стека не зависит от длины списка.

...