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 try
una singola riga come return
un'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.