【问题标题】:Python 3.7: How to avoid stackoverflow for this recursive approach?Python 3.7:如何避免这种递归方法的堆栈溢出?
【发布时间】:2019-03-31 23:20:22
【问题描述】:

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

此功能不会花费很长时间运行,因此不会冻结 GUI 和其他进程。取而代之的是,该函数几乎立即退出,并在稍后以很短的时间执行其工作。最后 - 当工作完成时 - 它调用提供的回调。

 

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 信号)。主线程一一处理事件队列条目,调用相应的槽。因此,如果软件向该队列中触发了太多事件,GUI 可能会变得无响应。

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

  • 阻塞事件队列,导致 GUI 无响应。
  • 递归函数帧溢出堆栈。

【问题讨论】:

  • 是否有错误信息让您认为堆栈空间不足?
  • 我确实遇到过几次崩溃,提到 recursion depth 太深了。因为项目中的实际功能比上面的示例更复杂,我不确定它是否也适用于这个示例。
  • 它不是递归的,因为QTimer.singleShot 是(我假设)立即返回,而不是调用process_next 并在返回之前等待it 返回。不过,我不知道self.foo 可能会对process_next 做什么。
  • @chepner,这是有道理的。我希望你是对的。但是既然QTimer调用了一个内部函数,那这个内部函数的上下文不应该保存在某个地方吗?如果是,那不是在堆栈上吗? 【PS:不用担心foo()。也许我应该删除它。]
  • 上下文可能是一个类似堆栈的结构,但它会被复制到堆中,而不是保存在调用堆栈中。此外,上下文中的变量是静态地确定的;只有它们的 取决于运行时上下文。

标签: python python-3.x recursion tail-recursion


【解决方案1】:

您没有包含item.foobarself.foo 的代码。假设这些调用不会引起深度递归,那么这段代码执行期间的最大堆栈深度不会随着列表的长度而增加。

functools.partial 不会立即调用process_next 函数。它只创建一个可以稍后调用的类似函数的对象。见https://docs.python.org/3/library/functools.html

QTimer.singleShot 也不会立即调用process_next 函数。它安排从functools.partial 返回的类函数对象在当前对process_next 的调用返回之后执行。

您可以通过在process_next 的开头添加一个print("enter") 语句并在返回之前添加一个print("leave") 语句来轻松验证这一点。

在递归的情况下,你会看到:

enter
enter
enter
...
leave
leave
leave

如果列表很长,堆栈会溢出。

如果没有递归,你会看到:

enter
leave
enter
leave
enter
leave
...

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

【讨论】:

    猜你喜欢
    • 2013-06-28
    • 2011-08-15
    • 2010-11-30
    • 1970-01-01
    • 2015-11-22
    • 1970-01-01
    • 2017-08-26
    • 2020-03-08
    • 2015-11-14
    相关资源
    最近更新 更多