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


206

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

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も実行しないと結論付けるべきでしょうか、それとも単に異なる方法で定義する必要があるだけでしょうか?


11
@Wessie TCOは、言語がどの程度動的または静的であるかを単純に考慮したものです。たとえば、Luaもそれを行います。あなたは単にテールコールを認識し(ASTレベルとバイトコードレベルの両方でかなりシンプル)、新しいものを作成する代わりに現在のスタックフレームを再利用する必要があります(シンプルで、ネイティブコードよりもインタープリターで実際にはさらにシンプルです) 。

11
ああ、1つのヒント:末尾再帰について排他的に話しますが、頭字語「TCO」を使用します。これは、末尾呼び出しの最適化を意味し、再帰であるかどうかに関係なく、(明示的または暗黙的に)すべてのインスタンスに適用されますreturn func(...)。TCOはTREの適切なスーパーセットであり、より有用であり(たとえば、TREができない継続渡しスタイルを実現可能にします)、実装はそれほど難しくありません。

1
ここではそれを実装するためのハック方法です-離れて実行フレームをスローするように上げ例外を使用してデコレータは:metapython.blogspot.com.br/2010/11/...
jsbueno

2
自分を末尾再帰に制限する場合、適切なトレースバックが非常に役立つとは思いません。あなたはfoofooから内fooから内から内からへの呼び出しfooを持っています...これを失うことで有用な情報が失われるとは思いません。
ケビン

1
私は最近ココナッツについて学びましたが、まだそれを試していません。一見の価値ありです。末尾再帰の最適化があると主張されています。
Alexey 2017年

回答:


216

いいえ。Guidovan Rossumが適切なトレースバックを行えることを好むため、そうなることは決してありません。

テール再帰の排除(2009-04-22)

末尾呼び出しの最後の言葉(2009-04-27)

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

>>> 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

12
または、そのように変換する場合-ただ:from operator import add; reduce(add, xrange(n + 1), csum)
Jon Clements

38
この特定の例で機能する@JonClements。whileループへの変換は、通常、末尾再帰に機能します。
John La Rooy 2013年

25
+1正解ですが、これは信じられないほど骨の折れる設計決定のようです。与えられた理由はに煮詰めるように見える「それはPythonが解釈される方法を与えないのは難しいと私はそこにとにかくそれ好きではありません!」
基本的な

12
@jwgだから...何?悪い設計上の決定についてコメントする前に、言語を書く必要がありますか?論理的または実用的とは思えない。私はあなたのコメントから、これまでに書かれた言語の機能(またはその欠如)について意見がないと思いますか?
基本的な

2
@基本いいえ、コメントしている記事を読む必要があります。それがあなたにどのように「要約」されるかを考えると、あなたが実際にそれを読んでいないことは非常に強く思われます。(残念ながら、いくつかの議論は両方に広がっているので、実際には両方のリンクされた記事を読む必要があるかもしれません。)言語の実装とはほとんど関係ありませんが、意図したセマンティクスと関係があります。
Veky

179

末尾呼び出しの最適化を実行するモジュールを公開しました(末尾再帰と継続渡しの両方のスタイルを処理します):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コンビネータのように見えます。


あなたの方法を使用して、いくつかの条件に基づいて他のいくつかの関数の1つをテールコールする関数を(できれば通常の定義と同様に)定義する例を提供していただけませんか?また、ラッピング関数bet0をクラスメソッドのデコレータとして使用できますか?
Alexey

@Alexeyコメント内にブロックスタイルでコードを記述できるかどうかはわかりませんが、もちろんdef関数の構文を使用できます。実際には、上記の最後の例は条件に依存しています。私の投稿baruchel.github.io/python/2015/11/07/…で、「もちろん、誰もそのようなコードを書かないことに異議を唱えることはできます」で始まる段落を見ることができます。ここで、通常の定義構文の例を示します。質問の2番目の部分では、しばらくその時間を費やしていないので、もう少し考えなければなりません。よろしく。
トーマスバルチェル2018年

TCO以外の言語の実装を使用している場合でも、関数のどこで再帰呼び出しが発生するかを気にする必要があります。これは、再帰呼び出しの後に発生する関数の部分が、スタックに格納する必要がある部分だからです。したがって、関数を末尾再帰にすると、再帰呼び出しごとに格納する必要のある情報の量が最小限に抑えられ、必要に応じて再帰呼び出しスタックを大きくする余地が広がります。
josiah

21

グイドの言葉はhttp://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.htmlにあります

私は最近、Pythonの機能的機能の起源についての記事をPython Historyブログに投稿しました。TREをPythonに追加できることを「証明」しようとしている他のユーザーによる最近のブログエントリへのリンクなど、末尾再帰の削除(TRE)をサポートしないことについてのサイドリマークは、Pythonがこれを実行しないのは残念だといういくつかのコメントをすぐに引き起こしました簡単に。私の立場を守らせてください(言語でTREを使いたくない)。短い答えが必要な場合は、単純にunpythonicです。これが長い答えです:


12
そしてここにいわゆるBDsFLの問題があります。
Adam Donahue

6
@AdamDonahue委員会からのすべての決定に完全に満足していますか?少なくとも、BDFLから正当で正当な説明が得られます。
Mark Ransom

2
いいえ、もちろんそうではありませんが、彼らは私をより平等に打ったように私を襲います。これは記述主義者からではなく、記述主義者からのものです。皮肉。
Adam Donahue

6

CPythonは、この件に関するGuido van Rossumの発言に基づく末尾呼び出しの最適化をサポートしませんし、おそらくサポートしません。

スタックトレースがどのように変更されるため、デバッグがより困難になるという意見を聞いたことがあります。


18
@mux CPythonは、プログラミング言語Pythonのリファレンス実装です。同じ言語を実装するが実装の詳細が異なる他の実装(PyPy、IronPython、Jythonなど)があります。(理論的には)TCOを実行する代替のPython実装を作成できるため、この区別はここで役立ちます。私はそれについて考えさえしている人を知りません、そして、それに依存するコードが他のすべてのPython実装で壊れるので、有用性は制限されます。


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.