Я опублікував модуль, що виконує оптимізацію хвостових викликів (обробка стилю рекурсії та стилю продовження): https://github.com/baruchel/tco
Оптимізація хвостової рекурсії в Python
Часто стверджувалося, що рекурсія хвоста не відповідає пітонічному способу кодування і що не слід дбати про те, як вставити його в цикл. Я не хочу сперечатися з цією точкою зору; інколи, однак, мені подобається намагатися або реалізовувати нові ідеї як хвостово-рекурсивні функції, а не циклі з різних причин (зосереджуючись на ідеї, а не на процесі, маючи двадцять коротких функцій на моєму екрані одночасно, а не лише три «пітонічні» функції, що працюють в інтерактивному сеансі, а не редагування мого коду тощо).
Оптимізація хвостової рекурсії в Python насправді досить проста. Хоча, як кажуть, це неможливо або дуже хитро, я думаю, що це можна досягти за допомогою елегантних, коротких та загальних рішень; Я навіть думаю, що більшість цих рішень не використовують функції Python інакше, ніж повинні. Чисті лямбда-вирази, що працюють разом із дуже стандартними петлями, призводять до швидких, ефективних та повністю застосованих інструментів для здійснення оптимізації хвостових рекурсій.
В якості особистої зручності я написав невеликий модуль, що реалізує таку оптимізацію двома різними способами. Я хотів би тут обговорити дві мої основні функції.
Чистий спосіб: модифікація комбінатора 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
Функцію можна використовувати наступним чином; ось два приклади з рекурсивними версіями факторіалу та Фібоначчі:
>>> 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
Звичайно, це єдина реальна мета функції.
З цією оптимізацією неможливо зробити лише одне: вона не може використовуватися з хвостово-рекурсивною функцією, що оцінює іншу функцію (це пов'язано з тим, що повернуті об'єкти, що викликаються, усі обробляються як подальші рекурсивні виклики без різниці). Оскільки мені зазвичай така функція не потрібна, я дуже задоволений наведеним вище кодом. Однак, щоб надати більш загальний модуль, я подумав трохи більше, щоб знайти певне вирішення цього питання (див. Наступний розділ).
Щодо швидкості цього процесу (що, однак, не є справжньою проблемою), це, здається, дуже добре; хвостово-рекурсивні функції оцінюються навіть набагато швидше, ніж за допомогою наступного коду за допомогою більш простих виразів:
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
Я думаю, що оцінювати один вираз, навіть складний, набагато швидше, ніж оцінювати кілька простих виразів, що є випадком у цій другій версії. Я не зберігав цю нову функцію у своєму модулі, і не бачу обставин, де вона могла б бути використана, а не "офіційна".
Продовження стилю проходження з винятками
Ось більш загальна функція; він здатний обробляти всі хвостово-рекурсивні функції, включаючи ті, що повертають інші функції. Рекурсивні дзвінки розпізнаються за іншими значеннями повернення за допомогою виключень. Це рішення повільніше, ніж попереднє; швидший код, ймовірно, може бути записаний, використовуючи деякі особливі значення, оскільки "прапори" виявляються в основному циклі, але мені не подобається ідея використання спеціальних значень або внутрішніх ключових слів. Існує забавна інтерпретація використання винятків: якщо Python не любить рекурсивні дзвінки, слід створити виняток, коли виникає хвостово-рекурсивний виклик, і пітонічним способом буде зловити виняток, щоб знайти чистий рішення, яке насправді відбувається тут ...
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
одного рядка як return
висловлювання: ми намагаємось щось повернути (нормальна поведінка), але ми не можемо цього зробити через рекурсивний виклик, що відбувається (виняток).
Початкова відповідь (2013-08-29).
Я написав дуже маленький плагін для обробки хвостової рекурсії. Ви можете знайти його з моїми поясненнями там: https://groups.google.com/forum/?hl=fr#!topic/comp.lang.python/dIsnJ2BoBKs
Він може вбудовувати лямбда-функцію, написану в стилі рекурсії хвоста, в іншій функції, яка буде оцінювати її як цикл.
На мою скромну думку, найцікавішою особливістю цієї невеликої функції є те, що ця функція не покладається на якийсь брудний хакер програмування, а лише на обчислення лямбда: поведінка функції змінюється на іншу, коли вона вставляється в іншу лямбда-функцію, яка дуже схоже на комбінатор Y.