web-dev-qa-db-fra.com

Est-ce que Python optimise la récursion des queues?

J'ai le morceau de code suivant qui échoue avec l'erreur suivante:

RuntimeError: profondeur maximale de récursivité dépassée

J'ai essayé de réécrire ceci pour permettre l'optimisation de la récursion de la queue (TCO). Je crois que ce code aurait dû réussir si un TCO avait eu lieu.

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

print(trisum(1000, 0))

Devrais-je en conclure que Python ne fait aucun type de coût total de possession, ou dois-je simplement le définir différemment?

175
Jordan Mack

Non, et ce ne sera jamais le cas depuis que Guido préfère pouvoir avoir des retraits appropriés.

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

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

Vous pouvez éliminer manuellement la récursion avec une transformation comme celle-ci.

>>> 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): Avec le temps, ma réponse est devenue très populaire et depuis, il s'agissait plus d'un lien que de Pour le reste, j’ai décidé de prendre un peu de temps et de le réécrire complètement (cependant, la réponse initiale se trouve à la fin).

Edit (2015-07-12): J'ai finalement publié un module d'optimisation de l'appel final (gestion à la fois du style de récursion de queue et du style de passage de continuation): https://github.com/baruchel/tco

Optimiser la récursion de la queue en Python

On a souvent prétendu que la récursion de la queue ne convenait pas à la méthode de codage Pythonic et qu'il ne fallait pas se soucier de la manière de l'intégrer dans une boucle. Je ne veux pas discuter avec ce point de vue; Parfois, cependant, j'aime essayer ou implémenter de nouvelles idées en tant que fonctions non récursives plutôt qu'avec des boucles pour différentes raisons (mettre l'accent sur l'idée plutôt que sur le processus, avoir vingt fonctions courtes sur mon écran en même temps plutôt que trois "Pythonic" fonctions, travaillant dans une session interactive plutôt que d’éditer mon code, etc.).

Optimiser la récursion de la queue dans Python est en fait assez facile. Bien que cela soit dit impossible ou très délicat, je pense que cela peut être réalisé avec des solutions élégantes, courtes et générales; je pense même que La plupart de ces solutions n'utilisent pas les fonctionnalités Python autrement qu'elles ne le devraient. Les expressions lambda propres qui fonctionnent avec des boucles très standard conduisent à des outils rapides, efficaces et pleinement utilisables pour la mise en œuvre de l'optimisation de la récursion finale.

Pour des raisons personnelles, j’ai écrit un petit module mettant en œuvre une telle optimisation de deux manières différentes. Je voudrais discuter ici de mes deux fonctions principales.

La voie propre: modifier le Y Combinator

Le Y Combinator est bien connu; cela permet d'utiliser les fonctions lambda de manière récursive, mais ne permet pas à lui seul d'incorporer des appels récursifs dans une boucle. Le calcul lambda seul ne peut pas faire une chose pareille. Un léger changement dans le Y Combinator peut toutefois protéger l'appel récursif pour qu'il soit réellement évalué. L'évaluation peut donc être retardée.

Voici la célèbre expression pour le Y Combinator:

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

Avec un très léger changement, je pourrais obtenir:

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

Au lieu de s’appeler elle-même, la fonction f renvoie maintenant une fonction effectuant le même appel, mais comme elle le renvoie, l’évaluation peut être effectuée ultérieurement de l’extérieur.

Mon code est:

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

La fonction peut être utilisée de la manière suivante. Voici deux exemples avec des versions récursives de factoriel et de 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

Evidemment, la profondeur de récursivité n'est plus un problème:

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

C’est bien entendu l’unique objectif réel de la fonction.

Une seule chose ne peut pas être réalisée avec cette optimisation: elle ne peut pas être utilisée avec une fonction récursive évaluant une autre fonction (cela vient du fait que les objets renvoyables appelables sont tous traités comme des appels récursifs ultérieurs, sans distinction). Comme je n'ai généralement pas besoin d'une telle fonctionnalité, je suis très content du code ci-dessus. Cependant, afin de fournir un module plus général, j’ai réfléchi un peu plus afin de trouver une solution de contournement à ce problème (voir la section suivante).

En ce qui concerne la rapidité de ce processus (qui n’est cependant pas le vrai problème), il s’avère plutôt bon; Les fonctions tail-récursives sont même évaluées beaucoup plus rapidement qu'avec le code suivant en utilisant des expressions plus simples:

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

Je pense qu'évaluer une expression, même compliquée, est beaucoup plus rapide que d'évaluer plusieurs expressions simples, ce qui est le cas dans cette deuxième version. Je n'ai pas conservé cette nouvelle fonction dans mon module et je ne vois aucune circonstance où elle pourrait être utilisée plutôt que la fonction "officielle".

Style de passage continu avec exceptions

Voici une fonction plus générale; il est capable de gérer toutes les fonctions de la queue, y compris celles renvoyant d'autres fonctions. Les appels récursifs sont reconnus à partir d'autres valeurs de retour par l'utilisation d'exceptions. Cette solution est plus lente que la précédente; un code plus rapide pourrait probablement être écrit en utilisant des valeurs spéciales en tant que "drapeaux" détectés dans la boucle principale, mais je n'aime pas l'idée d'utiliser des valeurs spéciales ou des mots-clés internes. Il existe une interprétation amusante de l’utilisation des exceptions: si Python n’aime pas les appels de type queue-récursif, une exception doit être déclenchée lorsqu’un appel de type queue-récursif a lieu, et la méthode Pythonic sera la suivante: attraper l'exception afin de trouver une solution propre, qui est en fait ce qui se passe ici ...

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

Toutes les fonctions peuvent maintenant être utilisées. Dans l'exemple suivant, f(n) est évalué en tant que fonction d'identité pour toute valeur positive de n:

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

Bien sûr, on pourrait faire valoir que les exceptions ne sont pas destinées à être utilisées pour réorienter intentionnellement l'interprète (comme une sorte d'énoncé goto ou probablement plutôt comme une sorte de style de passage continu), ce que je dois admettre. Mais, encore une fois, je trouve drôle d’utiliser try avec une seule ligne étant une instruction return: nous essayons de retourner quelque chose (comportement normal) mais nous ne pouvons pas le faire à cause d’un appel récursif en cours (exception).

Réponse initiale (2013-08-29).

J'ai écrit un très petit plugin pour gérer la récursion de la queue. Vous y trouverez peut-être mes explications: https://groups.google.com/forum/?hl=fr#!topic/comp.lang.python/dIsnJ2BoBKs

Il peut intégrer une fonction lambda écrite avec un style de récursion de queue dans une autre fonction qui l'évaluera en tant que boucle.

La caractéristique la plus intéressante de cette petite fonction, à mon humble avis, est que la fonction ne repose pas sur un programme mal conçu, mais sur un simple calcul lambda: le comportement de la fonction est remplacé par un autre lorsqu'il est inséré dans une autre fonction lambda qui ressemble beaucoup au Y-combinator.

Cordialement.

155
Thomas Baruchel

La Parole de Guido est à http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html

J'ai récemment posté une entrée dans mon Python), un blog d'histoire sur les origines des fonctions fonctionnelles de Python. Une remarque à propos de la non prise en charge de l'élimination de la récursion de la queue (TRE) a immédiatement suscité plusieurs commentaires à propos de ce dommage. Python ne le fait pas, y compris des liens vers des entrées de blog récentes de personnes essayant de "prouver" que TRE peut être ajouté à Python facilement. Laissez-moi donc Défendre ma position (c'est-à-dire que je ne veux pas de TRE dans la langue). Si vous voulez une réponse courte, c'est simplement non-rythmique. Voici la réponse longue:

20
Jon Clements

CPython ne prend pas et ne supportera probablement jamais l'optimisation des appels en queue basée sur les déclarations de Guido sur le sujet. J'ai entendu des arguments selon lesquels cela rend le débogage plus difficile en raison de la façon dont il modifie la trace de la pile.

6
recursive

Essayez la version expérimentale macropie TCO pour la taille.

3
Mark Lawrence

Outre l'optimisation de la récursion de la queue, vous pouvez définir manuellement la profondeur de la récursivité en:

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