Python 3.7:如何避免这种递归方法的堆栈溢出?



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

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

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

最新更新