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

时间:2019-03-30 16:46:35

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

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无法响应。
  • 使用递归函数框架溢出堆栈。

1 个答案:

答案 0 :(得分: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
...

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