末尾呼び出しの最適化を実行するモジュールを公開しました(末尾再帰と継続渡しの両方のスタイルを処理します):https : //github.com/baruchel/tco
Pythonでの末尾再帰の最適化
多くの場合、末尾再帰はPythonのコーディング方法に適さず、ループに埋め込む方法を気にする必要がないと言われています。私はこの見方に異議を唱えたくありません。ただし、さまざまな理由で、ループではなく末尾再帰関数として新しいアイデアを試したり実装したりすることが好きな場合があります(プロセスではなくアイデアに焦点を当て、画面に20個の短い関数を3つだけの「Pythonic」ではなく同時に表示させる関数、コードの編集などではなく、インタラクティブセッションでの作業など)。
Pythonでの末尾再帰の最適化は、実際には非常に簡単です。それは不可能または非常にトリッキーであると言われていますが、エレガントで短く、一般的なソリューションで実現できると思います。これらのソリューションのほとんどは、本来の目的以外ではPython機能を使用しないとさえ思っています。非常に標準的なループと共に動作するきれいなラムダ式は、末尾再帰の最適化を実装するための迅速で効率的で完全に使用可能なツールにつながります。
個人の便宜として、2つの異なる方法でこのような最適化を実装する小さなモジュールを作成しました。ここで、私の2つの主な機能について説明します。
クリーンな方法:Yコンビネーターを変更する
Yコンビネータはよく知られています。ラムダ関数を再帰的に使用することはできますが、ループ自体に再帰呼び出しを埋め込むことはできません。ラムダ計算だけではそのようなことはできません。ただし、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
この関数は次のように使用できます。以下は、factorialとFibonacciの末尾再帰バージョンの2つの例です。
>>> 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
これはもちろん、関数の単一の真の目的です。
この最適化で実行できないことは1つだけです。これは、別の関数に評価する末尾再帰関数では使用できません(これは、呼び出し可能な返されたオブジェクトがすべて区別なしにさらに再帰呼び出しとして処理されるためです)。私は通常そのような機能を必要としないので、上記のコードにとても満足しています。ただし、より一般的なモジュールを提供するために、この問題の回避策を見つけるためにもう少し考えました(次のセクションを参照)。
このプロセスの速度(これは本当の問題ではありません)に関しては、非常に優れています。末尾再帰関数は、より単純な式を使用する次のコードよりもはるかに速く評価されます。
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
この2番目のバージョンのように、1つの式を評価することは、たとえ複雑であっても、いくつかの単純な式を評価するよりもはるかに速いと思います。私はこの新しい関数をモジュールに保存していませんでした。また、「公式」関数よりも使用できる状況はありません。
例外を伴う継続渡しスタイル
次に、より一般的な関数を示します。他の関数を返すものを含む、すべての末尾再帰関数を処理できます。再帰呼び出しは、例外を使用することにより、他の戻り値から認識されます。このソリューションは、以前のソリューションよりも時間がかかります。メインループで検出される「フラグ」としていくつかの特別な値を使用することで、より速いコードを書くことができますが、特別な値や内部キーワードを使用するという考えは好きではありません。例外の使用にはいくつかの面白い解釈があります。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
が、1行をreturn
ステートメントとして使用するという考えがおかしいことに気づきました。何かを返そうとしますが(通常の動作)、再帰呼び出しが発生するため(例外)、それを行うことはできません。
最初の回答(2013-08-29)。
末尾再帰を処理するための非常に小さなプラグインを作成しました。あなたは私の説明でそれを見つけるかもしれません:https : //groups.google.com/forum/?hl=fr#!topic / comp.lang.python / dIsnJ2BoBKs
ループとして評価する別の関数に、末尾再帰スタイルで記述されたラムダ関数を埋め込むことができます。
私の控えめな意見では、この小さな関数の最も興味深い機能は、関数がいくつかの汚いプログラミングハックに依存せず、単なるラムダ計算に依存することです。関数の動作は、別のラムダ関数に挿入されると別のラムダ関数に変更されますYコンビネータのように見えます。