Python遞迴優化方法

2022-04-10 19:57:25 字數 3240 閱讀 3804

python的遞迴呼叫棧的深度有限制,預設深度為998,可以通過sys.getrecursionlimit()檢視。

針對遞迴棧溢位,我們可以將預設深度設定為大一些,這樣不會報錯,但是再大的深度總歸是有限的,而且深度越大對記憶體的占用也就越大,這對我們的程式是不利的。所以一般情況下我們不要將棧的深度設定太大。

但有時候我們又需要無限但遞迴,這裡我們就可以用到尾遞迴。

尾遞迴在很多語言中都可以被編譯器優化, 基本都是直接復用舊的執行棧, 不用再建立新的棧幀, 原理上其實也很簡單, 因為尾遞迴在本質上看的話遞迴呼叫是整個子過程呼叫的最後執行語句, 所以之前的棧幀的內容已經不再需要, 完全可以被復用。

需要注意的是, 一定記住尾遞迴的特點: 遞迴呼叫是整個子過程呼叫的最後一步,return的時候不能出現計算。否則就不是真正的尾遞迴了, 如下就不是真正的尾遞迴, 雖然遞迴呼叫出現在尾部

def

fib(n):

if n ==0:

return

0

elif n == 1:

return 1

else

:

return fib(n-1) + fib(n-2)

很明顯遞迴呼叫並不是整個計算過程的最後一步, 計算fib(n)是需要先遞迴求得fib(n-1)和fib(n-2), 然後做一步加法才能得到最終的結果。

如下是尾遞迴

def

fib(n, a, b):

if n == 1:

return

a

else

:

return fib(n-1, b, a+b)

上面那串紅字是什麼意思呢,即使你用了尾遞迴的語法寫了一串遞迴的**,但是最後還是會報深度問題的錯,因為python的原始碼中並沒有整合對尾遞迴的支援。。。

怎麼辦呢?有幾種解決辦法:

修改原始碼,如果你不怕會有後續錯誤的話。。

將上述**最後的return改為yield,然後在呼叫的時候用next,利用生成器實現。(會出乙個問題,如果遞迴的函式需要傳參,而引數是會變化的話,你會發現每次呼叫引數都不會變。。)

往下看~~

關於python中的尾遞迴呼叫有一段神奇的**:

import

sysclass

tailcallexception(baseexception):

def__init__

(self, args, kwargs):

self.args =args

self.kwargs =kwargs

deftail_call_optimized(func):

f =sys._getframe()

if f.f_back and f.f_back.f_back and f.f_code ==f.f_back.f_back.f_code:

raise

tailcallexception(args, kwargs)

else

:

while

true:

try:

return func(*args, **kwargs)

except

tailcallexception, e:

args =e.args

kwargs =e.kwargs

return

@tail_call_optimized

deffib(n, a, b):

if n == 1:

return

a

else

:

return fib(n-1, b, a+b)

r = fib(1200, 0, 1) #不報錯!

突破了呼叫棧的深度限制!只要加上裝飾器,尾遞迴就實現了!!

嗯,沒錯,就是這麼簡單。以後要想實現尾遞迴都時候就複製上面裝飾器以上都**,然後將遞迴函式加上該裝飾器就ok。

以上的**是怎樣的工作的呢?

理解它需要對python虛擬機器的函式呼叫有一定的理解。其實以上**和其他語言對尾遞迴的呼叫的優化原理都是相似的,那就是在尾遞迴呼叫的時候重複使用舊的棧幀, 因為之前說過, 尾遞迴本身在呼叫過程中, 舊的棧幀裡面那些內容已經沒有用了, 所以可以被復用。

python的函式呼叫首先要了解code object、function object、frame object這三個object(物件),

另外乙個需要注意到的是, 對於任何對尾遞迴而言, 其執行過程可以線性展開, 此時你會發現, 最終結果的產生完全可以從任意中間狀態開始計算, 最終都能得到同樣的執行結果。如果把函式引數看作狀態(state_n)的話, 也就是tail_call(state_n)->tail_call(state_n-1)->tail_call(state_n-2)->...->tail_call(state_0), state_0是遞迴臨界條件, 也就是遞迴收斂的最終狀態, 而你在執行過程中, 從任一起始狀態(state_n)到收斂狀態(state_0)的中間狀態state_x開始遞迴, 都可以得到同樣的結果。

當python執行過程中發生異常(錯誤)時(或者也可以直接手動丟擲raise ...), 該異常會從當前棧幀開始向舊的執行棧幀傳遞, 直到有乙個舊的棧幀捕獲這個異常, 而該棧幀之後(比它更新的棧幀)的棧幀就被**了。

有了以上的理論基礎, 就能理解之前**的邏輯了:

由於我們是第一次呼叫, 所以」if f.f_back and f.f_back.f_back and f.f_code == f.f_back.f_back.f_code」這句裡f.f_code==f.f_back.f_back.f_code顯然不滿足。

繼續走迴圈, 內部呼叫func(*args, **kwargs), 之前說過這個func是沒被裝飾器裝飾的fib, 也就是real_fib。

if f.f_back and f.f_back.f_back and f.f_code == f.f_back.f_back.f_code

在整個遞迴過程中, 沒有頻繁的遞迴一次, 生成乙個幀, 如果你不用這個優化, 可能你遞迴1000次, 就要生成1000個棧幀, 一旦達到遞迴棧的深度限制, 就掛了。

使用了這個裝飾器之後, 最多生成3個幀, 隨後就被**了, 所以是不可能達到遞迴棧的深度的限制的。

注意:這個裝飾器只能針對尾遞迴使用。

python遞迴 Python 與尾遞迴優化

有很多時候,使用遞迴的方式寫 要比迭代更直觀一些,以下面的階乘為例 def factorial n if n 0 return 1 return factorial n 1 n 但是這個函式呼叫,如果展開,會變成如下的形式 factorial 4 factorial 3 4 factorial 2 ...

Python 的尾遞迴優化

有很多時候,使用遞迴的方式寫 要比迭代更直觀一些,以下面的階乘為例 def factorial n if n 0 return 1return factorial n 1 n 但是這個函式呼叫,如果展開,會變成如下的形式 factorial 4 factorial 3 4 factorial 2 3...

python遞迴函式的優化

儘管遞迴可以通過迴圈來實現,但是往往遞迴 更加簡潔,邏輯更加清晰,先來看一段python遞迴 def fact n if n 1 return 1 else return fact n 1 n print fact 5 該遞迴呼叫的過程如下 計算機在呼叫函式時會使用堆疊,每呼叫乙個函式會增加一層棧幀...