Оптимизирует ли Python хвостовую рекурсию?

У меня есть следующий фрагмент кода, который не выполняется со следующей ошибкой:

RuntimeError: превышена максимальная глубина рекурсии

Я попытался переписать это, чтобы разрешить оптимизацию хвостовой рекурсии (TCO). Я считаю, что этот код должен был быть успешным, если была проведена ТШО.

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

print(trisum(1000, 0))

Должен ли я заключить, что Python не делает никакого типа TCO, или мне просто нужно определить его по-другому?

Ответ 1

Нет, и это никогда не будет, так как Гвидо предпочитает иметь надлежащие трассировки

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

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

Вы можете вручную исключить рекурсию с таким преобразованием

>>> 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

Ответ 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.

С уважением.

Ответ 3

Слово Гвидо находится в http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html

Недавно я опубликовал запись в моем блоге истории Python о происхождении Функциональные возможности Python. Боковое замечание об отсутствии поддержки хвоста устранение рекурсии (TRE) сразу вызвало несколько комментариев о как жаль, что Python этого не делает, включая ссылки на недавние записи в блоге другими, пытающимися "доказать", что TRE можно добавить на Python легко. Поэтому позвольте мне защищать свою позицию (а это то, что я не хочу TRE на языке). Если вам нужен короткий ответ, просто unpythonic. Вот длинный ответ:

Ответ 4

CPython не поддерживает и, вероятно, никогда не будет поддерживать оптимизацию хвостового вызова на основе заявлений Guido по этому вопросу. Я слышал аргументы, что это затрудняет отладку из-за того, как он изменяет трассировку стека.

Ответ 5

Попробуйте экспериментальную macropy реализацию TCO для размера.

Ответ 6

Кроме оптимизации хвостовой рекурсии, вы можете установить глубину рекурсии вручную:

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