it-swarm.com.de

Optimiert Python die Schwanzrekursion?

Ich habe den folgenden Code, der mit dem folgenden Fehler fehlschlägt:

RuntimeError: Maximale Rekursionstiefe überschritten

Ich habe versucht, dies neu zu schreiben, um die TCO (Tail Recursion Optimization) zu ermöglichen. Ich glaube, dieser Code hätte erfolgreich sein müssen, wenn eine TCO stattgefunden hätte.

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

print(trisum(1000, 0))

Sollte ich zu dem Schluss kommen, dass Python keine TCO ausführt, oder muss ich es nur anders definieren?

175
Jordan Mack

Nein, und das wird es auch nie, da Guido es vorzieht, über die richtigen Rückverfolgungen zu verfügen

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

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

Mit einer solchen Transformation können Sie die Rekursion manuell beseitigen

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

Edit (2015-07-02): Mit der Zeit ist meine Antwort sehr populär geworden und da sie anfangs eher ein Link war als Ansonsten habe ich beschlossen, etwas Zeit in Anspruch zu nehmen und es komplett neu zu schreiben (die erste Antwort finden Sie jedoch am Ende).

Edit (2015-07-12): Endlich habe ich ein Modul veröffentlicht, das die Tail-Call-Optimierung durchführt (sowohl die Tail-Rekursion als auch den Continuation-Passing-Stil): https://github.com/baruchel/tco

Optimierung der Schwanzrekursion in Python

Es wurde oft behauptet, dass die Schwanzrekursion nicht für die pythonische Art der Codierung geeignet ist und dass man sich nicht darum kümmern sollte, wie sie in eine Schleife eingebettet wird. Ich möchte mit diesem Standpunkt nicht streiten. Manchmal mag ich es jedoch aus verschiedenen Gründen, neue Ideen als rekursive Funktionen zu versuchen oder zu implementieren, anstatt mit Schleifen (ich konzentriere mich eher auf die Idee als auf den Prozess, habe zwanzig kurze Funktionen zur gleichen Zeit auf meinem Bildschirm anstatt nur drei "Pythonic" Funktionen, Arbeiten in einer interaktiven Sitzung, anstatt meinen Code zu bearbeiten usw.).

Die Optimierung der Schwanzrekursion in Python ist in der Tat recht einfach. Es wird zwar als unmöglich oder sehr schwierig bezeichnet, aber ich denke, dass dies mit eleganten, kurzen und allgemeinen Lösungen erreicht werden kann; ich denke sogar, dass Die meisten dieser Lösungen verwenden die Funktionen von Python nicht anders als sie sollten. Bereinigte Lambda-Ausdrücke in Verbindung mit sehr standardmäßigen Schleifen führen zu schnellen, effizienten und voll verwendbaren Tools zur Implementierung der Schwanzrekursionsoptimierung.

Aus persönlichen Gründen habe ich ein kleines Modul geschrieben, das eine solche Optimierung auf zwei verschiedene Arten implementiert. Ich möchte hier auf meine beiden Hauptfunktionen eingehen.

Der saubere Weg: Modifizieren des Y-Kombinators

Der Y-Kombinator ist allgemein bekannt. es erlaubt die rekursive Verwendung von Lambda-Funktionen, es erlaubt jedoch nicht, rekursive Aufrufe in eine Schleife einzubetten. Lambda-Kalkül allein kann so etwas nicht. Eine geringfügige Änderung des Y-Kombinators kann jedoch den tatsächlich auszuwertenden rekursiven Aufruf schützen. Die Auswertung kann somit verzögert werden.

Hier ist der berühmte Ausdruck für den Y-Kombinator:

lambda f: (lambda x: x(x))(lambda y: f(lambda *args: y(y)(*args)))

Mit einer sehr kleinen Änderung könnte ich bekommen:

lambda f: (lambda x: x(x))(lambda y: f(lambda *args: lambda: y(y)(*args)))

Anstatt sich selbst aufzurufen, gibt die Funktion f jetzt eine Funktion zurück, die denselben Aufruf ausführt, aber da sie ihn zurückgibt, kann die Auswertung später von außen erfolgen.

Mein Code ist:

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

Die Funktion kann auf folgende Weise verwendet werden; Hier sind zwei Beispiele mit rekursiven Versionen von Fakultät und Fibonacci:

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

Offensichtlich ist die Rekursionstiefe kein Problem mehr:

>>> bet( lambda f: lambda n: 42 if not n else f(n-1) )(50000)
42

Dies ist natürlich der einzige eigentliche Zweck der Funktion.

Mit dieser Optimierung kann nur eines nicht getan werden: Sie kann nicht mit einer Tail-Recursive-Funktion verwendet werden, die eine andere Funktion auswertet (dies ergibt sich aus der Tatsache, dass aufrufbare zurückgegebene Objekte alle als weitere rekursive Aufrufe ohne Unterscheidung behandelt werden). Da ich ein solches Feature normalerweise nicht benötige, bin ich mit dem obigen Code sehr zufrieden. Um jedoch ein allgemeineres Modul bereitzustellen, habe ich mir etwas mehr überlegt, um eine Lösung für dieses Problem zu finden (siehe nächster Abschnitt).

Was die Geschwindigkeit dieses Prozesses angeht (was jedoch nicht das eigentliche Problem ist), so ist er zufällig recht gut. Schwanzrekursive Funktionen werden mit einfacheren Ausdrücken sogar viel schneller ausgewertet als mit dem folgenden Code:

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

Ich denke, dass das Bewerten eines Ausdrucks, auch wenn er kompliziert ist, viel schneller ist als das Bewerten mehrerer einfacher Ausdrücke, was in dieser zweiten Version der Fall ist. Ich habe diese neue Funktion nicht in meinem Modul behalten, und ich sehe keine Umstände, unter denen sie eher als die "offizielle" verwendet werden könnte.

Weitergabe mit Ausnahmen

Hier ist eine allgemeinere Funktion; Es ist in der Lage, alle rekursiven Funktionen zu verarbeiten, einschließlich derjenigen, die andere Funktionen zurückgeben. Rekursive Aufrufe werden anhand von Ausnahmen von anderen Rückgabewerten erkannt. Diese Lösung ist langsamer als die vorherige. Ein schnellerer Code könnte wahrscheinlich geschrieben werden, indem einige spezielle Werte als "Flags" in der Hauptschleife erkannt werden, aber ich mag die Idee, spezielle Werte oder interne Schlüsselwörter zu verwenden, nicht. Es gibt eine witzige Interpretation der Verwendung von Ausnahmen: Wenn Python keine tail-rekursiven Aufrufe mag, sollte eine Ausnahme ausgelöst werden, wenn ein tail-rekursiver Aufruf auftritt, und dies wird auf pythonische Weise geschehen nimm die Ausnahme, um eine saubere Lösung zu finden, was hier tatsächlich passiert ...

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

Jetzt können alle Funktionen verwendet werden. Im folgenden Beispiel wird f(n) als Identitätsfunktion für jeden positiven Wert von n ausgewertet:

>>> f = bet0( lambda f: lambda n: (lambda x: x) if not n else f(n-1) )
>>> f(5)(42)
42

Man könnte natürlich argumentieren, dass Ausnahmen nicht dazu gedacht sind, den Interpreter absichtlich umzuleiten (als eine Art goto -Anweisung oder wahrscheinlich eher als eine Art Continuation-Passing-Stil), was ich zugeben muss. Aber auch hier finde ich die Idee, try mit einer einzelnen Zeile als return -Anweisung zu verwenden, lustig: Wir versuchen, etwas zurückzugeben (normales Verhalten), können dies jedoch aufgrund von nicht tun rekursiver Aufruf erfolgt (Ausnahme).

Erste Antwort (29.08.2013).

Ich habe ein sehr kleines Plugin für die Behandlung der Schwanzrekursion geschrieben. Sie können es mit meinen Erklärungen dort finden: https://groups.google.com/forum/?hl=fr#!topic/comp.lang.python/dIsnJ2BoBKs

Es kann eine Lambda-Funktion, die mit einem Schwanzrekursionsstil geschrieben wurde, in eine andere Funktion einbetten, die sie als Schleife auswertet.

Das interessanteste Merkmal dieser kleinen Funktion ist meiner bescheidenen Meinung nach, dass die Funktion nicht auf einem schmutzigen Programmier-Hack beruht, sondern auf einer reinen Lambda-Rechnung: Das Verhalten der Funktion wird geändert, wenn sie in eine andere Lambda-Funktion eingefügt wird, die sieht dem Y-Kombinator sehr ähnlich.

Grüße.

155
Thomas Baruchel

Das Wort von Guido lautet http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html

Ich habe kürzlich einen Eintrag in meinem Python History-Blog über die Ursprünge der Funktionsmerkmale von Python veröffentlicht. Eine Randbemerkung, dass TRE (Tail Recursion Elimination) nicht unterstützt wird, hat sofort einige Kommentare darüber ausgelöst, wie schade das ist Python tut dies nicht, einschließlich Links zu aktuellen Blogeinträgen von anderen, die zu "beweisen" versuchen, dass TRE leicht zu Python= hinzugefügt werden kann. Also lassen Sie es mich verteidige meine Position (was bedeutet, dass ich TRE nicht in der Sprache will.) Wenn du eine kurze Antwort willst, ist es einfach unpythonisch. Hier ist die lange Antwort:

20
Jon Clements

CPython unterstützt und wird wahrscheinlich niemals die Tail-Call-Optimierung basierend auf Guidos Aussagen zu diesem Thema unterstützen. Ich habe Argumente gehört, die das Debuggen erschweren, weil dadurch der Stack-Trace geändert wird.

6
recursive

Versuchen Sie die experimentelle Makropie TCO-Implementierung für Größe.

3
Mark Lawrence

Neben der Optimierung der Schwanzrekursion können Sie die Rekursionstiefe wie folgt manuell einstellen:

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