任何递归程序都可以成为堆栈安全的
我写了很多关于递归的文章,当人们错误地陈述事实时我很难过。不,这不依赖于 sys.setrecursionlimit() 这样的愚蠢技术。
在 python 中调用函数会添加一个堆栈帧。所以我们不会写f(x)来调用函数,而是写call(f,x)。现在我们可以完全控制评估策略 -
# btree.py
def depth(t):
if not t:
return 0
else:
return call \
( lambda left_height, right_height: 1 + max(left_height, right_height)
, call(depth, t.left)
, call(depth, t.right)
)
实际上是完全相同的程序。那么call是什么?
# tailrec.py
class call:
def __init__(self, f, *v):
self.f = f
self.v = v
所以call 是一个具有两个属性的简单对象:调用函数f 和调用它的值v。这意味着 depth 返回一个 call 对象,而不是我们需要的数字。只需要再调整一次 -
# btree.py
from tailrec import loop, call
def depth(t):
def aux(t): # <- auxiliary wrapper
if not t:
return 0
else:
return call \
( lambda l, r: 1 + max(l, r)
, call(aux, t.left)
, call(aux, t.right)
)
return loop(aux(t)) # <- call loop on result of aux
循环
现在我们需要做的就是编写一个足够熟练的loop 来评估我们的call 表达式。这里的答案是我在this Q&A (JavaScript) 中写的评估器的直接翻译。我不会在这里重复我自己,所以如果你想了解它是如何工作的,我会在我们在那篇文章中构建 loop 时逐步解释它 -
# tailrec.py
from functools import reduce
def loop(t, k = identity):
def one(t, k):
if isinstance(t, call):
return call(many, t.v, lambda r: call(one, t.f(*r), k))
else:
return call(k, t)
def many(ts, k):
return call \
( reduce \
( lambda mr, e:
lambda k: call(mr, lambda r: call(one, e, lambda v: call(k, [*r, v])))
, ts
, lambda k: call(k, [])
)
, k
)
return run(one(t, k))
注意到一个模式? loop 与 depth 一样递归,但我们在这里也使用 call 表达式进行递归。注意loop 如何将其输出发送到run,在那里发生了明确无误的迭代 -
# tailrec.py
def run(t):
while isinstance(t, call):
t = t.f(*t.v)
return t
检查你的工作
from btree import node, depth
# 3
# / \
# 9 20
# / \
# 15 7
t = node(3, node(9), node(20, node(15), node(7)))
print(depth(t))
3
堆栈与堆
您不再受 python 堆栈限制 ~1000 的限制。我们有效地劫持了 python 的评估策略并编写了我们自己的替代品loop。我们没有将函数调用帧扔到堆栈上,而是将它们换成堆上的延续。现在唯一的限制是您计算机的内存。