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

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

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

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

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

print(trisum(1000, 0))

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

6 ответов


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

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

изменить (2015-07-02): со временем мой ответ стал довольно популярным, и поскольку изначально это была скорее Ссылка, чем что-либо еще, я решил взять некоторое время и переписать его полностью (однако первоначальный ответ можно найти в конце).

Edit (2015-07-12): я, наконец, опубликовал модуль, выполняющий оптимизацию хвостового вызова (обработка как хвостовой рекурсии, так и стиля продолжения): https://github.com/baruchel/tco

оптимизация хвостовой рекурсии в Python

неоднократно утверждалось, что хвост-рекурсия не устраивает подходящие для 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

это, конечно, единственная реальная цель функции.

только одна вещь может быть сделано с этим оптимизация: его нельзя использовать с tail-рекурсивная функция, оценивающая другую функцию (это происходит из факта все вызываемые возвращаемые объекты обрабатываются как дальнейшие рекурсивные вызовы с помощью без различия). Поскольку мне обычно не нужна такая функция, я очень счастлив с кодом выше. Однако, чтобы обеспечить более общий модуль, я подумал немного больше, чтобы найти обходной путь для этой проблемы (см. Следующий раздел).

относительно скорости этого процесса (что, однако, не является реальной проблемой), это происходит чтобы быть довольно хорошим; хвост-рекурсивные функции даже оцениваются намного быстрее, чем с следующий код с использованием более простых выражений:

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-комбинатор.

С уважением.


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

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


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


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


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

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