web-dev-qa-db-ja.com

Pythonは末尾再帰を最適化しますか?

次のエラーで失敗する次のコードがあります。

RuntimeError:最大再帰深度を超えました

これを書き換えて、末尾再帰最適化(TCO)を可能にしようとしました。 TCOが行われていれば、このコードは成功していたはずです。

def trisum(n, csum):
    if n == 0:
        return csum
    else:
        return trisum(n - 1, csum + n)

print(trisum(1000, 0))

PythonはどのタイプのTCOも実行しないと結論付ける必要がありますか、それとも別に定義する必要がありますか?

175
Jordan Mack

いいえ、グイドは適切なトレースバックを持つことができることを好むため、それは決してありません

http://neopythonic.blogspot.com.au/2009/04/tail-recursion-elimination.html

http://neopythonic.blogspot.com.au/2009/04/final-words-on-tail-calls.html

このような変換を使用して、再帰を手動で削除できます

>>> def trisum(n, csum):
...     while True:                     # change recursion to a while loop
...         if n == 0:
...             return csum
...         n, csum = n - 1, csum + n   # update parameters instead of tail recursion

>>> trisum(1000,0)
500500
184
John La Rooy

編集(2015-07-02):時間が経つにつれて、私の答えはかなり人気になり、最初はもっと他の何よりもリンクなので、少し時間をかけて完全に書き直すことにしました(ただし、最初の答えは最後にあります)。

Edit(2015-07-12):最後に、末尾呼び出しの最適化を実行するモジュールを公開しました(末尾再帰と継続渡しの両方のスタイルを扱います): https://github.com/baruchel/tco

Pythonでの末尾再帰の最適化

多くの場合、末尾再帰はPythonのコーディング方法に適していないと主張されており、ループに埋め込む方法を気にする必要はありません。この観点で議論したくありません。ただし、さまざまな理由でループではなく、末尾再帰関数として新しいアイデアを試したり実装したりするのが好きな場合があります(プロセスではなくアイデアに焦点を当て、3つの「Pythonic」ではなく、同時に20個の短い関数を画面に表示します機能、コードの編集などではなく、インタラクティブセッションでの作業など)。

Pythonでの末尾再帰の最適化は実際、非常に簡単です。それは不可能または非常に難しいと言われていますが、エレガントで短く、一般的なソリューションで達成できると思います。これらのソリューションのほとんどは、Python機能を必要以上に使用していないと思います。非常に標準的なループとともに機能するクリーンなラムダ式は、末尾再帰最適化を実装するための迅速で効率的で完全に使用可能なツールにつながります。

個人的な便宜のため、このような最適化を2つの異なる方法で実装する小さなモジュールを作成しました。ここで、2つの主な機能について説明します。

クリーンな方法:Y Combinatorの変更

Y Combinatorはよく知られています。ラムダ関数を再帰的に使用できますが、ループ内に再帰呼び出しを埋め込むことはできません。ラムダ計算だけではそのようなことはできません。ただし、Y Combinatorのわずかな変更により、実際に評価される再帰呼び出しを保護できます。したがって、評価が遅れることがあります。

Y Combinatorの有名な表現は次のとおりです。

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

この関数は次の方法で使用できます。以下に、階乗とフィボナッチの末尾再帰バージョンの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

複雑な式であっても、1つの式の評価は、この2番目のバージョンの場合のように、いくつかの単純な式の評価よりもはるかに速いと思います。私は自分のモジュールにこの新しい関数を保持しませんでした。「公式」関数ではなく、使用できる状況は見当たりません。

例外を伴う継続渡しスタイル

より一般的な機能は次のとおりです。他の関数を返す関数を含む、すべての末尾再帰関数を処理できます。再帰呼び出しは、例外の使用により他の戻り値から認識されます。このソリューションは、以前のソリューションよりも低速です。メインループで検出される「フラグ」としていくつかの特別な値を使用することにより、おそらくより速いコードを書くことができますが、特別な値または内部キーワードを使用するという考えは好きではありません。例外の使用には面白い解釈があります:Pythonが末尾再帰呼び出しを好まない場合、末尾再帰呼び出しが発生したときに例外を発生させる必要があり、Pythonの方法は例外をキャッチすることですきれいな解決策を見つけるために.

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コンビネーターに非常に似ています。

よろしく。

155
Thomas Baruchel

Word of Guidoは http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html にあります

最近、私のPython Historyブログに、Pythonの機能的特徴の起源に関するエントリを投稿しました。テール再帰除去(TRE)をサポートしないことについての副次的な発言は、Pythonがこれを行わないことに関する残念なコメントをすぐに引き起こしました。 TREはPythonに簡単に追加できます。それで、私の立場を守らせてください(つまり、言語でTREを使いたくないのです)。短い答えが必要な場合、それは単純です。長い答えは次のとおりです。

20
Jon Clements

CPythonは、サブジェクトに関するGuidoの声明に基づいたテールコールの最適化をサポートしておらず、おそらくサポートしないでしょう。スタックトレースを変更する方法が原因でデバッグが難しくなるという議論を聞いたことがあります。

6
recursive

実験的な macropy サイズのTCO実装を試してください。

3
Mark Lawrence

末尾再帰を最適化するほか、次の方法で再帰の深さを手動で設定できます。

import sys
sys.setrecursionlimit(5500000)
print("recursion limit:%d " % (sys.getrecursionlimit()))
1
zhenv5