簡體   English   中英

Python 是否優化尾遞歸?

[英]Does Python optimize tail recursion?

我有以下一段代碼失敗並出現以下錯誤:

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,還是只需要以不同的方式定義它?

不,它永遠不會,因為Guido van Rossum更喜歡能夠有適當的回溯:

尾遞歸消除(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

我發布了一個執行尾調用優化的模塊(處理尾遞歸和連續傳遞樣式): https ://github.com/baruchel/tco

在 Python 中優化尾遞歸

人們經常聲稱尾遞歸不適合 Pythonic 的編碼方式,並且不應該關心如何將其嵌入循環中。 我不想與這種觀點爭論; 但有時我喜歡嘗試或實現新想法作為尾遞歸函數而不是循環出於各種原因(專注於想法而不是過程,在我的屏幕上同時有 20 個短函數而不是只有三個“Pythonic”功能,在交互式會話中工作而不是編輯我的代碼等)。

在 Python 中優化尾遞歸實際上很容易。 雖然據說這是不可能的或非常棘手的,但我認為可以通過優雅、簡短和通用的解決方案來實現; 我什至認為這些解決方案中的大多數都沒有使用 Python 功能,而不是它們應該使用的功能。 干凈的 lambda 表達式與非常標准的循環一起工作,為實現尾遞歸優化提供了快速、高效且完全可用的工具。

為了個人方便,我編寫了一個小模塊,通過兩種不同的方式實現這種優化。 我想在這里討論一下我的兩個主要功能。

干凈的方法:修改 Y 組合器

Y 組合器是眾所周知的。 它允許以遞歸方式使用 lambda 函數,但它本身不允許在循環中嵌入遞歸調用。 僅 Lambda 演算無法做到這一點。 然而,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 不喜歡尾遞歸調用,當發生尾遞歸調用時應該引發異常,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

現在所有功能都可以使用了。 在以下示例中,對於 n 的任何正值,將f(n)計算為恆等函數:

>>> f = bet0( lambda f: lambda n: (lambda x: x) if not n else f(n-1) )
>>> f(5)(42)
42

當然,可以說異常並不打算用於故意重定向解釋器(作為一種goto語句,或者可能更確切地說是一種延續傳遞風格),我必須承認這一點。 但是,再一次,我覺得使用單行作為return語句的try的想法很有趣:我們嘗試返回一些東西(正常行為)但我們不能這樣做,因為發生了遞歸調用(異常)。

初步答案(2013-08-29)。

我寫了一個非常小的插件來處理尾遞歸。 您可以在我的解釋中找到它: https ://groups.google.com/forum/?hl=fr#!topic/comp.lang.python/dIsnJ2BoBKs

它可以將用尾遞歸樣式編寫的 lambda 函數嵌入到另一個函數中,該函數會將其評估為循環。

在我看來,這個小函數中最有趣的特性是,該函數不依賴於一些骯臟的編程技巧,而是僅僅依賴於 lambda 演算:當插入另一個 lambda 函數時,該函數的行為會更改為另一個看起來很像 Y 組合器。

Guido 的話在http://neopythonic.blogspot.co.uk/2009/04/tail-recursion-elimination.html

我最近在我的 Python 歷史博客上發表了一篇關於 Python 函數特性起源的文章。 一個關於不支持尾遞歸消除 (TRE) 的評論立即引發了幾條評論,說 Python 不這樣做是多么遺憾,包括其他人試圖“證明” TRE 可以添加到 Python 的最近博客條目的鏈接容易地。 因此,讓我捍衛我的立場(即我不希望在語言中使用 TRE)。 如果您想要一個簡短的答案,那簡直是無稽之談。 這是長答案:

CPython不支持也可能永遠不會支持基於Guido van Rossum 關於該主題的陳述的尾調用優化。

我聽說過它使調試變得更加困難的論點,因為它修改了堆棧跟蹤。

除了優化尾遞歸之外,您還可以通過以下方式手動設置遞歸深度:

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

嘗試使用實驗性的macropy TCO 實現來確定大小。

Python 中沒有內置的尾遞歸優化。 但是,我們可以通過抽象語法樹(AST)“重建”函數,消除那里的遞歸並用循環替換它。 Guido 是絕對正確的,這種方法有一些局限性,所以我不推薦使用它。

但是,我仍然編寫(而不是作為訓練示例)我自己的優化器版本,您甚至可以嘗試它是如何工作的。

通過 pip 安裝這個包:

pip install astrologic

現在您可以運行此示例代碼:

from astrologic import no_recursion

counter = 0

@no_recursion
def recursion():
    global counter
    counter += 1
    if counter != 10000000:
        return recursion()
    return counter

print(recursion())

這個解決方案不穩定,你永遠不應該在生產中使用它。 您可以在 github 上閱讀有關頁面上的一些重要限制(俄語,抱歉)。 然而,這個解決方案是相當“真實”的,沒有中斷代碼和其他類似的技巧。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM