Чи оптимізує Python хвостові рекурсії?


206

У мене є такий фрагмент коду, який не вдається із наступною помилкою:

RuntimeError: перевищена максимальна глибина рекурсії

Я спробував це переписати, щоб дозволити оптимізацію хвостової рекурсії (TCO). Я вважаю, що цей код мав би бути успішним, якби відбувся ТСО.

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

print(trisum(1000, 0))

Чи варто зробити висновок, що Python не робить жодного типу TCO, чи мені просто потрібно визначити його по-іншому?


11
@ Wessie TCO - це простий погляд на те, наскільки динамічна чи статична мова. Наприклад, Луа теж робить це. Вам потрібно просто розпізнати хвостові дзвінки (досить прості, як на рівні AST, так і на рівні байт-коду), а потім повторно використовувати поточний кадр стека, а не створювати новий (також простий, насправді навіть простіший в інтерпретаторах, ніж у рідному коді) .

11
О, одна нитка: Ви говорите виключно про рекурсію хвоста, але використовуєте абревіатуру "TCO", що означає оптимізацію хвостових викликів і застосовується до будь-якого примірника return func(...)(явно чи неявно), будь то рекурсивна чи ні. TCO є належним набором TRE, і більш корисним (наприклад, це робить стиль продовження передачі можливим, який TRE не може), і не набагато складніше втілити.

1
Ось хакейний спосіб його реалізації - декоратор, що використовує підняття винятків, щоб відкинути кадри виконання: metapython.blogspot.com.br/2010/11/…
jsbueno

2
Якщо ви обмежитеся рецидивом хвоста, я не вважаю, що правильний трекбек дуже корисний. У вас є дзвінок fooзсередини на дзвінок fooзсередини на fooвнутрішній виклик на foo... Я не думаю, що від втрати цього не втратиться жодна корисна інформація.
Кевін

1
Нещодавно я дізнався про кокосовий горіх, але ще не пробував. Здається, варто поглянути. Стверджується, що оптимізація хвостової рекурсії.
Олексій

Відповіді:


215

Ні, і це ніколи не станеться, оскільки Гвідо ван Россум вважає за краще мати належні сліди:

Ліквідація хвостової рекурсії (2009-04-22)

Заключні слова про виклики хвоста (2009-04-27)

Ви можете вручну усунути рекурсію за допомогою такого перетворення:

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

12
Або якщо ти збираєшся трансформувати це так - просто from operator import add; reduce(add, xrange(n + 1), csum):?
Джон Клементс

38
@JonClements, що працює в цьому конкретному прикладі. Перетворення циклу на деякий час працює для рекурсії хвоста в загальних випадках.
Джон Ла Рой

25
+1 Оскільки це правильна відповідь, але це здається неймовірно нестандартним дизайнерським рішенням. Наведені причини, схоже, зводяться до "важко зробити, враховуючи, як інтерпретується python, і мені це все одно не подобається!"
Основна

12
@jwg Отже ... Що? Ви повинні написати мову, перш ніж ви зможете прокоментувати погані дизайнерські рішення? Навряд чи здається логічним чи практичним. З вашого коментаря я припускаю, що ви не маєте жодної думки щодо будь-яких особливостей (або їх відсутності) будь-якою мовою, написаної колись
Основні

2
@Basic Ні, але ви повинні прочитати статтю, яку ви коментуєте. Дуже сильно здається, що ви насправді цього не читали, враховуючи, як він «кипить» для вас. (Можливо, вам, можливо, доведеться прочитати обидві пов’язані статті, на жаль, оскільки деякі аргументи поширюються на обидва.) Це майже не має нічого спільного з реалізацією мови, але все стосується передбачуваної семантики.
Векі

178

Я опублікував модуль, що виконує оптимізацію хвостових викликів (обробка стилю рекурсії та стилю продовження): 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.


Чи можете ви, будь ласка, надати приклад визначення функції (бажано подібною до звичайної дефініції), яка називає хвіст однією з кількох інших функцій на основі якоїсь умови, використовуючи ваш метод? Чи може ваша функція обгортання bet0використовуватись як декоратор для класових методів?
Олексій

@Alexey Я не впевнений, що я можу писати код у блоковому стилі всередині коментаря, але, звичайно, можна використовувати defсинтаксис для своїх функцій, і насправді останній приклад, наведений вище, покладається на умову. У моєму дописі baruchel.github.io/python/2015/11/07/… ви можете побачити абзац, що починається з "Звичайно, ви могли заперечити, щоб ніхто не писав такого коду", де я навожу приклад із звичайним синтаксисом визначення. У другій частині вашого запитання я мушу подумати над цим трохи більше, оскільки я не витрачав на це час. З повагою
Томас Баручель

Вам слід дбати про те, де у вашій функції відбувається рекурсивний виклик, навіть якщо ви використовуєте мову, яка не є TCO. Це тому, що частина функції, яка виникає після рекурсивного виклику, - це та частина, яку потрібно зберегти у стеку. Таким чином, введення в функцію хвостово-рекурсивного мінімізує кількість інформації, яку ви повинні зберігати за рекурсивний виклик, що дає вам більше місця для великих рекурсивних стеків викликів, якщо вони вам потрібні.
josiah

21

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

Нещодавно я опублікував запис у своєму блозі з історії Python про джерела функціональних особливостей Python. Побічне зауваження про непідтримку усунення хвостової рекурсії (TRE) негайно викликало кілька коментарів про те, що шкода, що Python цього не робить, включаючи посилання на останні записи в блозі інших, які намагаються "довести", що TRE можна додати до Python легко. Тож дозвольте мені відстояти свою позицію (це означає, що я не хочу TRE мовою). Якщо ви хочете короткої відповіді, це просто непітонічно. Ось довга відповідь:


12
І в цьому полягає проблема з так званими BDsFL.
Адам Донахю

6
@AdamDonahue Ви були абсолютно задоволені кожним рішенням комітету? Принаймні, ви отримаєте аргументоване та авторитетне пояснення від BDFL.
Марк Викуп

2
Ні, звичайно, ні, але вони вражають мене як рівномірнішого. Це від рецептуриста, а не дескриптивіста. Іронія.
Адам Донахю

6

CPython не підтримує і, ймовірно, ніколи не підтримуватиме оптимізацію хвостових викликів на основі заяв Гідо ван Россума з цього приводу.

Я чув аргументи, що це налагоджує налагодження складніше через те, як він змінює слід стека.


18
@mux CPython - це опорна реалізація мови програмування Python. Є й інші реалізації (такі як PyPy, IronPython та Jython), які реалізують ту саму мову, але відрізняються деталями реалізації. Відмінність тут корисна, оскільки (теоретично) можливо створити альтернативну реалізацію Python, яка робить TCO. Мені невідомо, що хтось навіть про це думає, і корисність буде обмежена, оскільки код, що спирається на нього, порушить усі інші реалізації Python.


2

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

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

5
Чому ви просто не використовуєте jQuery?
Джеремі Герт

5
Тому що він також не пропонує TCO? :-D stackoverflow.com/questions/3660577 / ...
Veky
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.