Python ottimizza la ricorsione della coda?


206

Ho il seguente pezzo di codice che non riesce con il seguente errore:

RuntimeError: superata la profondità massima di ricorsione

Ho provato a riscriverlo per consentire l'ottimizzazione della ricorsione della coda (TCO). Credo che questo codice avrebbe dovuto avere successo se si fosse verificato un TCO.

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

print(trisum(1000, 0))

Devo concludere che Python non fa alcun tipo di TCO o devo solo definirlo diversamente?


11
@Wessie TCO è semplice considerazione di quanto sia dinamica o statica la lingua. Lua, per esempio, lo fa anche. Devi semplicemente riconoscere le chiamate di coda (piuttosto semplici, sia a livello di AST che a livello di bytecode), e quindi riutilizzare il frame dello stack corrente invece di crearne uno nuovo (anche semplice, in realtà persino più semplice negli interpreti che nel codice nativo) .

11
Oh, uno scherzo: parli esclusivamente di ricorsione della coda, ma usi l'acronimo "TCO", che significa ottimizzazione delle chiamate di coda e si applica a qualsiasi istanza di return func(...)(esplicitamente o implicitamente), che sia ricorsiva o meno. Il TCO è un vero e proprio superset di TRE e più utile (ad es. Rende fattibile lo stile di passaggio di continuazione, che TRE non può) e non molto più difficile da implementare.

1
Ecco un modo hacker per implementarlo: un decoratore che utilizza l'eccezione per eliminare i frame di esecuzione: metapython.blogspot.com.br/2010/11/…
jsbueno

2
Se ti limiti alla ricorsione della coda, non penso che un vero traceback sia super utile. Hai una chiamata foodall'interno di una chiamata foodall'interno a foodall'interno di una chiamata a foo... Non penso che nessuna informazione utile sarebbe persa a causa della perdita di questo.
Kevin,

1
Di recente ho imparato a conoscere il cocco ma non l'ho ancora provato. Vale la pena dare un'occhiata. Si dice che abbia l'ottimizzazione della ricorsione della coda.
Alexey,

Risposte:


216

No, e non lo sarà mai dal momento che Guido van Rossum preferisce essere in grado di avere delle risposte adeguate:

Eliminazione della ricorsione della coda (22-04-2009)

Final Words on Tail Calls (2009-04-27)

Puoi eliminare manualmente la ricorsione con una trasformazione come questa:

>>> 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
O se hai intenzione di trasformarlo in quel modo - solo from operator import add; reduce(add, xrange(n + 1), csum):?
Jon Clements

38
@JonClements, che funziona in questo esempio particolare. La trasformazione in un ciclo while funziona per la ricorsione della coda in casi generali.
John La Rooy,

25
+1 Per essere la risposta corretta, ma questa sembra una decisione di design incredibilmente ossea. Le ragioni date sembrano ridursi a "è difficile da fare dato come viene interpretato Python e non mi piace comunque così lì!"
Basic

12
@jwg Quindi ... Cosa? Devi scrivere una lingua prima di poter commentare decisioni di progettazione sbagliate? Sembra quasi logico o pratico. Presumo dal tuo commento che non hai opinioni su alcuna caratteristica (o mancanza) in una lingua mai scritta?
Base

2
@Basic No, ma devi leggere l'articolo su cui stai commentando. Sembra fortemente che tu non l'abbia letto, considerando come "si riduce" a te. (Potresti aver bisogno di leggere entrambi gli articoli collegati, sfortunatamente, poiché alcuni argomenti sono distribuiti su entrambi.) Non ha quasi nulla a che fare con l'implementazione del linguaggio, ma tutto ha a che fare con la semantica prevista.
Veky,

179

Ho pubblicato un modulo che esegue l'ottimizzazione della coda di chiamata (gestendo sia lo stile di ricorsione della coda che di passaggio di continuazione): https://github.com/baruchel/tco

Ottimizzare la ricorsione della coda in Python

È stato spesso affermato che la ricorsione della coda non si adatta al modo di codifica Pythonic e che non si dovrebbe preoccuparsi di come incorporarlo in un ciclo. Non voglio discutere con questo punto di vista; a volte però mi piace provare o implementare nuove idee come funzioni ricorsive della coda piuttosto che con loop per vari motivi (concentrandomi sull'idea piuttosto che sul processo, avendo nello stesso tempo venti funzioni brevi sul mio schermo anziché solo tre "Pythonic" funzioni, lavorando in una sessione interattiva anziché modificare il mio codice, ecc.).

L'ottimizzazione della ricorsione della coda in Python è in effetti abbastanza semplice. Mentre si dice che sia impossibile o molto complicato, penso che possa essere raggiunto con soluzioni eleganti, brevi e generali; Penso anche che la maggior parte di queste soluzioni non usi le funzionalità di Python diversamente da come dovrebbero. Le espressioni lambda pulite che funzionano insieme a loop molto standard portano a strumenti rapidi, efficienti e completamente utilizzabili per implementare l'ottimizzazione della ricorsione della coda.

Per comodità personale, ho scritto un piccolo modulo che implementa tale ottimizzazione in due modi diversi. Vorrei discutere qui delle mie due funzioni principali.

Il modo più pulito: modificare il combinatore Y.

Il combinatore Y è ben noto; consente di utilizzare le funzioni lambda in modo ricorsivo, ma non consente da solo di incorporare le chiamate ricorsive in un ciclo. Il calcolo lambda da solo non può fare una cosa del genere. Un leggero cambiamento nel combinatore Y può tuttavia proteggere la chiamata ricorsiva da valutare effettivamente. La valutazione può quindi essere ritardata.

Ecco la famosa espressione per il combinatore Y:

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

Con un leggero cambiamento, potrei ottenere:

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

Invece di chiamare se stesso, la funzione f ora restituisce una funzione che esegue la stessa chiamata, ma poiché la restituisce, la valutazione può essere effettuata in seguito dall'esterno.

Il mio codice è:

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 funzione può essere utilizzata nel modo seguente; ecco due esempi con versioni ricorsive di fattoriale e 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

Ovviamente la profondità di ricorsione non è più un problema:

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

Questo è ovviamente l'unico vero scopo della funzione.

Solo una cosa non può essere fatta con questa ottimizzazione: non può essere usata con una funzione ricorsiva di coda che valuta un'altra funzione (questo deriva dal fatto che gli oggetti restituiti richiamabili sono tutti gestiti come ulteriori chiamate ricorsive senza distinzione). Dal momento che di solito non ho bisogno di una tale funzionalità, sono molto contento del codice sopra. Tuttavia, al fine di fornire un modulo più generale, ho pensato un po 'di più al fine di trovare qualche soluzione alternativa per questo problema (vedere la sezione successiva).

Per quanto riguarda la velocità di questo processo (che non è comunque il vero problema), sembra essere abbastanza buono; le funzioni ricorsive della coda vengono persino valutate molto più rapidamente rispetto al codice seguente usando espressioni più semplici:

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

Penso che la valutazione di un'espressione, anche complicata, sia molto più rapida rispetto alla valutazione di diverse espressioni semplici, come nel caso di questa seconda versione. Non ho conservato questa nuova funzione nel mio modulo e non vedo circostanze in cui possa essere utilizzata piuttosto che quella "ufficiale".

Continuazione passando stile con eccezioni

Ecco una funzione più generale; è in grado di gestire tutte le funzioni ricorsive della coda, comprese quelle che restituiscono altre funzioni. Le chiamate ricorsive vengono riconosciute da altri valori di ritorno mediante l'uso di eccezioni. Questa soluzione è più lenta della precedente; un codice più veloce potrebbe probabilmente essere scritto usando alcuni valori speciali come "flag" rilevati nel ciclo principale, ma non mi piace l'idea di usare valori speciali o parole chiave interne. Esiste una divertente interpretazione dell'uso delle eccezioni: se a Python non piacciono le chiamate ricorsive di coda, si dovrebbe sollevare un'eccezione quando si verifica una chiamata ricorsiva di coda, e il modo Pythonic sarà quello di catturare l'eccezione per trovare un po 'pulito soluzione, che in realtà è ciò che accade qui ...

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

Ora è possibile utilizzare tutte le funzioni. Nel seguente esempio, f(n)viene valutata la funzione di identità per qualsiasi valore positivo di n:

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

Naturalmente, si potrebbe sostenere che le eccezioni non sono destinate a essere utilizzate per reindirizzare intenzionalmente l'interprete (come una sorta di goto affermazione o probabilmente una sorta di stile di passaggio di continuazione), che devo ammettere. Ma, ancora una volta, trovo divertente l'idea di utilizzare tryuna singola riga come returnun'istruzione: proviamo a restituire qualcosa (comportamento normale) ma non possiamo farlo a causa di una chiamata ricorsiva (eccezione).

Risposta iniziale (29-08-2013).

Ho scritto un plugin molto piccolo per gestire la ricorsione della coda. Puoi trovarlo con le mie spiegazioni lì: https://groups.google.com/forum/?hl=fr#!topic/comp.lang.python/dIsnJ2BoBKs

Può incorporare una funzione lambda scritta con uno stile di ricorsione della coda in un'altra funzione che la valuterà come un ciclo.

La caratteristica più interessante di questa piccola funzione, secondo la mia modesta opinione, è che la funzione non si basa su un trucco di programmazione sporco ma su un semplice calcolo lambda: il comportamento della funzione viene cambiato in un altro quando inserito in un'altra funzione lambda che assomiglia molto al combinatore Y.


Potresti, per favore, fornire un esempio di definizione di una funzione (preferibilmente in modo simile a una definizione normale) che chiama in coda una di molte altre funzioni in base a una condizione, usando il tuo metodo? Inoltre, la tua funzione di avvolgimento può bet0essere utilizzata come decoratore per i metodi di classe?
Alexey,

@Alexey Non sono sicuro di poter scrivere codice in stile blocco all'interno di un commento, ma puoi ovviamente usare la defsintassi per le tue funzioni, e in realtà l'ultimo esempio sopra si basa su una condizione. Nel mio post baruchel.github.io/python/2015/11/07/… puoi vedere un paragrafo che inizia con "Naturalmente potresti obiettare che nessuno scriverebbe un tale codice" in cui faccio un esempio con la solita sintassi di definizione. Per la seconda parte della tua domanda, devo pensarci un po 'di più poiché non ci ho passato del tempo da un po'. Saluti.
Thomas Baruchel,

Dovresti preoccuparti di dove avviene la chiamata ricorsiva nella tua funzione anche se stai usando un'implementazione del linguaggio non TCO. Questo perché la parte della funzione che si verifica dopo la chiamata ricorsiva è la parte che deve essere memorizzata nello stack. Pertanto, rendere la funzione ricorsiva della coda riduce al minimo la quantità di informazioni che è necessario archiviare per chiamata ricorsiva, il che offre più spazio per disporre di grandi pile di chiamate ricorsive se necessario.
josiah,

21

La parola di Guido è su http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html

Di recente ho pubblicato una voce nel mio blog Python History sulle origini delle caratteristiche funzionali di Python. Un'osservazione laterale sul non supportare l'eliminazione della ricorsione della coda (TRE) ha immediatamente suscitato numerosi commenti su che peccato che Python non lo faccia, compresi i collegamenti a recenti articoli del blog da parte di altri che cercano di "dimostrare" che TRE può essere aggiunto a Python facilmente. Quindi lasciami difendere la mia posizione (che è che non voglio TRE nella lingua). Se vuoi una risposta breve, è semplicemente non ritmico. Ecco la lunga risposta:


12
E qui sta il problema con il cosiddetto BDsFL.
Adam Donahue,

6
@AdamDonahue sei stato perfettamente soddisfatto di ogni decisione che viene da un comitato? Almeno ottieni una spiegazione motivata e autorevole dal BDFL.
Mark Ransom,

2
No, certo che no, ma mi sembrano più equilibrati. Questo da un prescrittivista, non da un descrittivista. L'ironia.
Adam Donahue,

6

CPython non supporta e probabilmente non supporterà mai l'ottimizzazione delle chiamate di coda in base alle dichiarazioni di Guido van Rossum sull'argomento.

Ho sentito argomenti che rendono il debug più difficile a causa di come modifica la traccia dello stack.


18
@mux CPython è l'implementazione di riferimento del linguaggio di programmazione Python. Esistono altre implementazioni (come PyPy, IronPython e Jython), che implementano la stessa lingua ma differiscono nei dettagli dell'implementazione. La distinzione è utile qui perché (in teoria) è possibile creare un'implementazione alternativa di Python che fa il TCO. Non sono a conoscenza di nessuno che ci stia nemmeno pensando, e l'utilità sarebbe limitata poiché il codice che si basava su di esso si infrangerebbe su tutte le altre implementazioni di Python.


2

Oltre a ottimizzare la ricorsione della coda, è possibile impostare manualmente la profondità di ricorsione:

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

5
Perché non usi semplicemente jQuery?
Jeremy Hert,

5
Perché anche non offre TCO? :-D stackoverflow.com/questions/3660577/...
Veky
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.