У меня есть следующий фрагмент кода, который не выполняется со следующей ошибкой:
Я попытался переписать это, чтобы разрешить оптимизацию хвостовой рекурсии (TCO). Я считаю, что этот код должен был быть успешным, если была проведена ТШО.
Должен ли я заключить, что Python не делает никакого типа TCO, или мне просто нужно определить его по-другому?
Ответ 2
Изменить (2015-07-02): С течением времени мой ответ стал довольно популярным, и поскольку он был изначально более связным, чем что-либо еще, я решил заняться некоторым временем и повторить, напишите его полностью (однако исходный ответ можно найти в конце).
Изменить (2015-07-12): Наконец-то я опубликовал модуль, выполняющий оптимизацию хвостового вызова (обработка как хвостовой рекурсии, так и продолжения): https://github.com/baruchel/tco
Оптимизация хвостовой рекурсии в Python
Часто утверждалось, что хвостовая рекурсия не соответствует питоническому способу кодирования
и что не следует заботиться о том, как вставлять его в цикл. Я не хочу спорить с
эта точка зрения; иногда мне нравится пытаться или внедрять новые идеи
как хвостовые рекурсивные функции, а не с петлями по разным причинам (с упором на
а не на процесс, имея двадцать коротких функций на моем экране в том же
а не только три "пифонические" функции, работающие на
чем редактирование моего кода и т.д.).
Оптимизация хвостовой рекурсии в Python на самом деле довольно проста. Хотя говорят, что это невозможно
или очень сложно, я думаю, это может быть достигнуто с помощью элегантных, коротких и общих решений; я даже
что большинство этих решений не используют функции Python иначе, чем должны.
Чистые лямбда-выражения, работающие вместе с очень стандартными петлями, приводят к быстрому, эффективному и
полностью используемые инструменты для реализации оптимизации хвостовой рекурсии.
В качестве личного удобства я написал небольшой модуль, реализующий такую оптимизацию
двумя разными способами. Я хотел бы обсудить здесь о моих двух основных функциях.
Чистый способ: изменение Y combinator
Y combinator хорошо известен; он позволяет использовать лямбда-функции в рекурсивном
но он сам по себе не позволяет встраивать рекурсивные вызовы в цикл. лямбда
Исчисление само по себе не может этого сделать. Однако небольшое изменение в Y combinator
может защитить рекурсивный вызов, который будет фактически оценен. Таким образом, оценка может быть отложена.
Вот знаменитое выражение для Y combinator:
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-combinator.
С уважением.