如果您使用的是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
是用来执行修改后的"递归函数"(严格地说,它不再是解释器所看到的"递归").这个executor是universal.修改后的代码run_to_finish
可以用来调用任何递归函数.你可以把run_to_finish
‘S的定义放到一个库中,想用的时候就导入.
我们来分析一下修改后的代码的执行流程:
run_to_finish(recursive_add(10000))
通过generator到run_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))