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)
而不会出现以下任何问题:
- 阻塞事件队列导致 GUI 无响应。
- 递归函数帧溢出堆栈。
您没有包含 item.foobar
和 self.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
...
并且最大堆栈深度与列表的长度无关。
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.foobar
和 self.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
...
并且最大堆栈深度与列表的长度无关。