我发布了一个执行尾调用优化的模块(处理尾递归和继续传递样式):https://github.com/baruchel/tco
在 Python 中优化尾递归
经常有人声称尾递归不适合 Pythonic 的编码方式,而且人们不应该关心如何将它嵌入到循环中。我不想争论
这种观点;但有时我喜欢尝试或实施新想法
由于各种原因,作为尾递归函数而不是循环(专注于
想法而不是过程,在我的屏幕上同时有二十个简短的功能
时间而不是只有三个“Pythonic”函数,在交互式会话中工作而不是编辑我的代码等)。
在 Python 中优化尾递归实际上很容易。虽然据说不可能
或者非常棘手,我认为可以通过优雅、简短和通用的解决方案来实现;我什至
认为这些解决方案中的大多数都没有使用 Python 功能。
干净的 lambda 表达式与非常标准的循环一起工作,可以实现快速、高效和
实现尾递归优化的完全可用的工具。
为了个人方便,我写了一个小模块来实现这样的优化
通过两种不同的方式。我想在这里讨论一下我的两个主要功能。
干净的方法:修改 Y 组合器
Y combinator 是众所周知的;它允许在递归中使用 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
现在所有功能都可以使用了。在以下示例中,f(n) 被评估为
n 任何正值的恒等函数:
>>> f = bet0( lambda f: lambda n: (lambda x: x) if not n else f(n-1) )
>>> f(5)(42)
42
当然,可以说例外不是故意用于
重定向解释器(作为一种goto 语句,或者可能更确切地说是一种
延续传递风格),我不得不承认。但是,再次,
我觉得使用try 单行作为return 语句的想法很有趣:我们尝试返回
某事(正常行为),但由于发生递归调用(异常),我们不能这样做。
初步回答(2013-08-29)。
我写了一个非常小的插件来处理尾递归。你可能会在我的解释中找到它:https://groups.google.com/forum/?hl=fr#!topic/comp.lang.python/dIsnJ2BoBKs
它可以将用尾递归样式编写的 lambda 函数嵌入到另一个函数中,该函数将其评估为循环。
以我的拙见,这个小函数中最有趣的特性是该函数不依赖于一些肮脏的编程技巧,而是仅仅依赖于 lambda 演算:当插入另一个函数时,该函数的行为会更改为另一个lambda 函数,看起来很像 Y 组合器。