我发布了一个实现尾调用优化的模块(同时处理尾递归和延续传递风格):https://github.com/baruchel/tco
优化Python中的尾递归
经常有人声称,尾递归不适合Python式编程方式,而且不应该关心如何将其嵌入循环中。我不想就这个观点争论;但是有时候为了各种原因(专注于思路而不是过程,屏幕上同时显示二十个短函数而不是三个“Pythonic”函数,使用交互式会话而不是编辑代码等)我喜欢尝试或者实现新的想法作为尾递归函数,而不是用循环。
实际上,在Python中优化尾递归非常容易。虽然有人认为它是不可能或者非常棘手的,但我认为可以通过优雅、简洁和通用的解决方案来实现;甚至大多数解决方案不使用Python特性,而是使用非常标准的循环和干净的lambda表达式,从而实现快速、高效和完全可用的工具来实现尾递归优化。
出于个人方便,我编写了一个小模块来实现这种优化的两种不同方式。在这里,我想讨论一下我的两个主要函数。
干净的方法:修改Y组合器
Y组合器是众所周知的;它允许以递归方式使用lambda函数,但它本身不能将递归调用嵌入循环中。单独的lambda演算无法做到这一点。然而,对Y组合器进行轻微修改可以保护递归调用被实际评估。因此可以延迟评估。
以下是Y组合器的著名表达式:
lambda f: (lambda x: x(x))(lambda y: f(lambda *args: y(y)(*args)))
稍作更改,我就可以得到:
lambda f: (lambda x: x(x))(lambda y: f(lambda *args: lambda: y(y)(*args)))
函数f不再直接调用自身,而是返回一个执行相同操作的函数。由于该函数被返回,因此可以稍后从外部进行评估。
我的代码如下:
def bet(func):
b = (lambda f: (lambda x: x(x))(lambda y:
f(lambda *args: lambda: y(y)(*args))))(func)
def wrapper(*args):
out = b(*args)
while callable(out):
out = out()
return out
return wrapper
该函数可以按以下方式使用;下面是两个示例,分别是尾递归版本的阶乘和斐波那契数列:
>>> from recursion import *
>>> fac = bet( lambda f: lambda n, a: a if not n else f(n-1,a*n) )
>>> fac(5,1)
120
>>> fibo = bet( lambda f: lambda n,p,q: p if not n else f(n-1,q,p+q) )
>>> fibo(10,0,1)
55
显然,递归深度已不再是一个问题:
>>> bet( lambda f: lambda n: 42 if not n else f(n-1) )(50000)
42
这当然是函数的单一真正目的。
使用这种优化仅有一件事情是无法完成的:它不能与一个尾递归函数一起使用,该函数求值为另一个函数(这是因为返回的可调用对象都被视为进一步递归调用而没有区别)。由于我通常不需要这样的功能,所以我对上面的代码非常满意。但是,为了提供一个更普遍的模块,我思考了一下以找到一些解决此问题的方法(请参见下一节)。
关于这个过程的速度(虽然这不是真正的问题),它实际上相当快;尾递归函数的计算甚至比使用简单表达式的以下代码要快得多:
def bet1(func):
def wrapper(*args):
out = func(lambda *x: lambda: x)(*args)
while callable(out):
out = func(lambda *x: lambda: x)(*out())
return out
return wrapper
我认为评估一个表达式,即使是复杂的,比评估几个简单的表达式要快得多,而这正是第二个版本中的情况。我没有将这个新函数保留在我的模块中,也看不到任何情况可以使用它而不是“官方”函数。
带异常的续延传递风格
这是一个更通用的函数;它可以处理所有尾递归函数,包括返回其他函数的函数。通过使用异常来识别递归调用与其他返回值区分开来。这种解决方案比之前的解决方案慢一些;可能可以通过在主循环中使用一些特殊值作为“标志”来编写更快的代码,但我不喜欢使用特殊值或内部关键字的想法。对于使用异常的某些有趣的解释:如果Python不喜欢尾递归调用,在尾递归调用发生时应该引发异常,并且Pythonic的方式将捕获异常以找到一些干净的解决方案,这实际上就是这里发生的事情...
class _RecursiveCall(Exception):
def __init__(self, *args):
self.args = args
def _recursiveCallback(*args):
raise _RecursiveCall(*args)
def bet0(func):
def wrapper(*args):
while True:
try:
return func(_recursiveCallback)(*args)
except _RecursiveCall as e:
args = e.args
return wrapper
现在可以使用所有函数。在下面的例子中,对于任何正值n,f(n)
都会被计算为恒等函数:
>>> f = bet0( lambda f: lambda n: (lambda x: x) if not n else f(n-1) )
>>> f(5)(42)
42
当然,可以说异常并不是用于有意地重定向解释器(作为一种类似于goto语句或可能更像 continuation passing style的语句),我得承认。但是,再次强调,我觉得使用单行代码的try语句来实现return语句很有趣:我们试图返回某些东西(正常行为),但由于发生递归调用(异常),我们无法实现。
初始回答(2013-08-29)。
我编写了一个非常小的插件来处理尾递归。您可以在我的解释中找到它:https://groups.google.com/forum/?hl=fr#!topic/comp.lang.python/dIsnJ2BoBKs
它可以将使用尾递归风格编写的lambda函数嵌入到另一个函数中,该函数将其评估为循环。
在我看来,这个小函数中最有趣的特点是,它不依赖于某些肮脏的编程技巧,而是仅依赖于lambda演算:当插入到另一个非常类似于Y组合子的lambda函数中时,函数的行为会改变为另一个函数。
return func(...)
(无论是否递归)的情况。TCO 是 TRE 的超集,更为实用(例如,它使得 CPS 可行,而 TRE 不能),且实现难度不大。 - user395760foo
函数内又调用了一次foo
函数,然后再从一个foo
函数内部调用另一个foo
函数...... 在失去这个信息后,并不会丢失任何有用的信息。 - Kevin