我们有一个包含相当多递归函数的Python代码库,它们可能会遇到深度递归,并使用默认的Python解释器设置来打破recursion limit.在过go ,我们使用了sys.setrecursionlimit()来使它们工作得很好.但现在这些功能需要放在一个共享的模块中,运行在其他用户的环境中.我们意识到在共享模块的代码中调用sys.setrecursionlimit()可能不是一个好主意,因为这将改变我们用户的全局设置.即使我们总是在函数返回之前更改回设置,用户可能正在运行多线程程序,并且只在其中一个线程中调用我们的函数,他们可能不希望其他线程受到设置更改的影响.(如果我的理解有误,请纠正我)

因此,我们期待有一些方法可以使我们的递归函数工作,同时保持系统递归限制不变.

在问这个问题之前,我在互联网上搜索了一下,找到了很多信息.它们真的很有用,到目前为止,我可以从这些资源中得出两个结论:

  1. 如果可能的话,使用sys.setrecursionlimit()来更改递归限制,或者使用类似context manager的技巧来更改此设置,然后恢复.--如前所述,我们不认为这是共享模块的最佳 Select .

  2. 将递归函数的代码改为迭代.大多数帖子建议手动更改代码,首先研究函数,然后逐个重构它们.这是可能的,但它需要大量的人工工作,因为我们的代码库不小,而且一些递归函数相当复杂.This post显示了将复杂的递归函数转换为迭代函数是多么困难的一个例子.但当然,如果有必要,我们将依靠这种最后的手段,但现在我们正在寻找其他通用的方法.

当说"泛型"时,我的意思是只需要简单的更改,不需要深入理解函数的逻辑,也不需要完全手工设计迭代代码.这里的"通用"并不意味着很小的代码更改,它意味着更少的手动工作,或"自动".

some posts中,我发现可以通过使用名为TCO的库来消除尾递归.因此,只需添加一个修饰符并进行一些通用的文本替换,就可以更改所有的尾递归函数.这与我们正在寻找的解决方案非常接近.我还学习了trampoline,它也适用于尾递归.然而,our codebase includes not only tail recursions.

这里有一个问题:在不调用sys.setrecursionlimit()的情况下,有没有一种方法可以让深度递归函数(不仅是尾递归函数)工作,并且只需最少的手动工作来更改遗留代码?有没有可以做到这一点的自动重构工具?

推荐答案

如果您使用的是Python3,而递归函数目前不包含yield表达式,那么有一种通用的方法,只需更改每个添加了yield的递归函数调用,并使用executor function作为客户端代码的调用入口点.修改后,解释器原来的调用堆栈将被显式列表取代(仅针对修改后的函数),递归限制仅取决于列表的最大大小.

关于屈服表达式的预先知识

我们先举个例子来看看yield的作用:

def func():
    y = yield 1
    return y


f = func()

try:
    x = next(f)
    print(x)
    f.send(x + 1)
except StopIteration as e:
    print(e.value)

当函数包含yield expression时,该函数的返回值自动变为generator.注意f = func()行,它看起来像是在执行func函数中的代码,但事实并非如此,它只返回一个generator,其中包含func函数的参数和状态,但不执行func中的任何行.

这一点很重要,此功能将用于消除递归调用.

在随后的try块中执行next(f)之后,实际执行func中的代码.next使func从该入口运行,直到遇到yield expression.在这种情况下,将yield之后的值赋给x作为返回值next(f),并且在yield 1处暂停func的执行.

f.send(x + 1)将值x + 1传递到func函数上次暂停的位置,并让func从该位置继续执行.在func内,x + 1被赋值为y,作为yield 1的值,然后执行return y.

func中的return y不是返回值,而是抛出StopIteration异常.异常对象中的value属性包含return之后的值.因此,在执行完return statement之后,执行将进入except StopIteration as e:代码块.最后,打印返回值2.

递归函数的修改

假设我们有一个如下所示的递归函数:

def recursive_add(x):
    return 0 if x == 0 else x + recursive_add(x - 1)

通过使用yield,我们对前面的递归函数进行了如下修改(还增加了executor function和调用示例):

def recursive_add(x):
    return 0 if x == 0 else x + (yield recursive_add(x - 1))


def run_to_finish(gen):
    call_stack = [gen]
    return_value = None  # Return value of the function at the innermost level

    while call_stack:
        try:
            if return_value is not None:
                inner_call = call_stack[-1].send(return_value)  # Transfer the return value of the inner function to the outer
            else:
                inner_call = next(call_stack[-1])  # The function is just being executed or does not require a return value
            call_stack.append(inner_call)
        except StopIteration as e:  # Inner function exit
            del call_stack[-1]
            return_value = e.value  # Obtains the return value

    return return_value


print(run_to_finish(recursive_add(10000)))

修改后的recursive_add与原来的不同之处在于递归调用中添加了一个yield关键字和一对圆括号(请注意,此处的圆括号不能省略).此通用方法适用于任何其他递归函数:change the recursive calls to yield expression, and leave all the other code as it is.

run_to_finish是用来执行修改后的"递归函数"(严格地说,它不再是解释器所看到的"递归").这个executoruniversal.修改后的代码run_to_finish可以用来调用任何递归函数.你可以把run_to_finish‘S的定义放到一个库中,想用的时候就导入.

我们来分析一下修改后的代码的执行流程:

run_to_finish(recursive_add(10000))通过generatorrun_to_finish executor.回想一下,当recursive_add内部有一个yield expression时,调用recursive_add只返回一个generator,并且不执行函数内部的代码.

run_to_finish开始运行,这将generator置于call_stack列表中.call_stack是记录函数的逻辑调用链的数据 struct (它们的每个状态都在堆栈上,包括局部变量等).它取代了Python解释器的公共调用堆栈.

接下来,只要call_stack不是空的,总是获取call_stack中的最后generator,并让它继续执行.call_stack中的generators被存储在函数的逻辑调用序列中.最后generator表示当前"最里面"的执行函数.

由于recursive_add已经转换为generator,因此每次执行yield recursive_add(x - 1)时,并不立即进入recursive_add的函数体,而是返回一个新的generator.这个generator被分配给inner_call并保存到call_stack.

return_value记录刚刚退出的函数的"返回值".(对于generator,它实际上是StopIteration异常对象中包含的value.)如果返回值存在,则使用send方法将其传递给调用链中的外层generator.

如果捕获到StopIteration异常,则当前最内层的函数退出.删除call_stack中对应的generator,并将返回值传递给"caller function".

recursive_add的输入参数为0时,recursive_add的内部代码将不会执行yield expression.在这种情况下,recursive_add仍然返回generator.然而,当它的next第一次被调用时,这个generator抛出一个异常(返回0的意思).

call_stack中的generators全部完成时,return_value是原始递归函数的最终结果.

遍历二叉树的另一个示例

这是另一个例子.请注意,the original function of traversing bin-tree is not a tail recursive function,但它仍然可以使用此方法修改和运行.run_to_finish Executor与前一个Executor完全相同,因此这里不显示定义.

class Node:
    def __init__(self, value, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right


def dfs(root):
    if root is None:
        return
    print(root.value)  # print value of this node
    yield dfs(root.left)  # traverse left child subtree, only yield is added for modification
    yield dfs(root.right)  # traverse right child subtree, only yield is added for modification


bin_tree = Node(1, Node(2, Node(3), Node(4)), Node(5))
run_to_finish(dfs(bin_tree))

Python相关问答推荐

如何将Pydantic URL验证限制为特定主机或网站

customtkinter中使用的这个小部件的名称是什么

sys.modulesgo 哪儿了?

有条件地采样我的大型DF的最有效方法

具有多个选项的计数_匹配

根据不同列的值在收件箱中移动数据

Pandas 都是(),但有一个门槛

Pandas - groupby字符串字段并按时间范围 Select

Mistral模型为不同的输入文本生成相同的嵌入

用合并列替换现有列并重命名

将pandas Dataframe转换为3D numpy矩阵

优化器的运行顺序影响PyTorch中的预测

实现自定义QWidgets作为QTimeEdit的弹出窗口

计算分布的标准差

使用groupby方法移除公共子字符串

启动带有参数的Python NTFS会导致文件路径混乱

为什么np. exp(1000)给出溢出警告,而np. exp(—100000)没有给出下溢警告?

使用BeautifulSoup抓取所有链接

人口全部乱序 - Python—Matplotlib—映射

具有相同图例 colored颜色 和标签的堆叠子图