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

Python 3.7: How to avoid stackoverflow for this recursive approach?

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) 而不会出现以下任何问题:

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

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

QTimer.singleShot 也不会立即调用 process_next 函数。它安排从 functools.partial 编辑的类似函数的对象 returned 稍后执行,在当前对 process_next 的调用 returned.

之后

您可以通过在 process_next 的开头放置一个 print("enter") 语句并在 return.[=24 之前放置一个 print("leave") 语句来轻松地自己验证这一点=]

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

enter
enter
enter
...
leave
leave
leave

对于很长的列表,堆栈会溢出。

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

enter
leave
enter
leave
enter
leave
...

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