Python 3.7:如何避免这种递归方法的stackoverflow?

问题描述 投票:1回答:1

1. The situation

我正在使用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

此功能不会花费很长时间来运行,因此它不会冻结GUI和其他进程。相反,该功能几乎立即退出,并在很多短时间内完成其工作。最后 - 当作业完成时 - 它调用提供的回调。

2. The problem

但是有一个缺点。这种方法给堆栈带来了很大的压力(我认为),因为你有以下链:

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信号)。主线程逐个处理事件队列条目,调用相应的槽。因此,如果软件向该队列发出过多事件,则GUI可能会无响应。

是否有另一种优雅的方式来调用process_next(myIterator)没有任何以下问题:

  • 阻塞事件队列,使GUI无响应。
  • 使用递归函数框架溢出堆栈。
python python-3.x recursion tail-recursion
1个回答
1
投票

您没有包含item.foobarself.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
...

并且最大堆栈深度与列表的长度无关。

© www.soinside.com 2019 - 2024. All rights reserved.