溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Python怎么開啟尾遞歸優(yōu)化

發(fā)布時間:2022-05-16 09:15:45 來源:億速云 閱讀:118 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要講解了“Python怎么開啟尾遞歸優(yōu)化”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“Python怎么開啟尾遞歸優(yōu)化”吧!

一般遞歸與尾遞歸

一般遞歸:

def normal_recursion(n):
    if n == 1:
        return 1
    else:
        return n + normal_recursion(n-1)

執(zhí)行:

normal_recursion(5)
5 + normal_recursion(4)
5 + 4 + normal_recursion(3)
5 + 4 + 3 + normal_recursion(2)
5 + 4 + 3 + 2 + normal_recursion(1)
5 + 4 + 3 + 3
5 + 4 + 6
5 + 10
15

可以看到, 一般遞歸, 每一級遞歸都產(chǎn)生了新的局部變量, 必須創(chuàng)建新的調(diào)用棧, 隨著遞歸深度的增加, 創(chuàng)建的棧越來越多, 造成爆棧?

尾遞歸

尾遞歸基于函數(shù)的尾調(diào)用, 每一級調(diào)用直接返回遞歸函數(shù)更新調(diào)用棧, 沒有新局部變量的產(chǎn)生, 類似迭代的實(shí)現(xiàn):

def tail_recursion(n, total=0):
    if n == 0:
        return total
    else:
        return tail_recursion(n-1, total+n)

執(zhí)行:

tail_recursion(5, 0)
tail_recursion(4, 5)
tail_recursion(3, 9)
tail_recursion(2, 12)
tail_recursion(1, 14)
tail_recursion(0, 15)
15

可以看到, 尾遞歸每一級遞歸函數(shù)的調(diào)用變成"線性"的形式. 這時, 我們可以思考, 雖然尾遞歸調(diào)用也會創(chuàng)建新的棧, 但是我們可以優(yōu)化使得尾遞歸的每一級調(diào)用共用一個棧!, 如此便可解決爆棧和遞歸深度限制的問題!

C中尾遞歸的優(yōu)化

gcc使用-O2參數(shù)開啟尾遞歸優(yōu)化:

int tail_recursion(int n, int total) {
    if (n == 0) {
        return total;
    }
    else {
        return tail_recursion(n-1, total+n);
    }
}
int main(void) {
    int total = 0, n = 4;
    tail_recursion(n, total);
    return 0;
}

反匯編

$ gcc -S tail_recursion.c -o normal_recursion.S
$ gcc -S -O2 tail_recursion.c -o tail_recursion.S gcc開啟尾遞歸優(yōu)化

對比反匯編代碼如下(AT&T語法, 左圖為優(yōu)化后)

Python怎么開啟尾遞歸優(yōu)化

可以看到, 開啟尾遞歸優(yōu)化前, 使用call調(diào)用函數(shù), 創(chuàng)建了新的調(diào)用棧(LBB0_3); 而開啟尾遞歸優(yōu)化后, 就沒有新的調(diào)用棧生成了, 而是直接pop bp指向的_tail_recursion函數(shù)的地址(pushq %rbp)然后返回, 仍舊用的是同一個調(diào)用棧!

Python開啟尾遞歸優(yōu)化

cpython本身不支持尾遞歸優(yōu)化, 但是一個牛人想出的解決辦法:

實(shí)現(xiàn)一個 tail_call_optimized 裝飾器

#!/usr/bin/env python2.4
# This program shows off a python decorator(
# which implements tail call optimization. It
# does this by throwing an exception if it is
# it's own grandparent, and catching such
# exceptions to recall the stack.
import sys
class TailRecurseException:
    def __init__(self, args, kwargs):
        self.args = args
        self.kwargs = kwargs
def tail_call_optimized(g):
    """
    This function decorates a function with tail call
    optimization. It does this by throwing an exception
    if it is it's own grandparent, and catching such
    exceptions to fake the tail call optimization.
    This function fails if the decorated
    function recurses in a non-tail context.
    """
    def func(*args, **kwargs):
        f = sys._getframe()
        if f.f_back and f.f_back.f_back \
            and f.f_back.f_back.f_code == f.f_code:
            # 拋出異常
            raise TailRecurseException(args, kwargs)
        else:
            while 1:
                try:
                    return g(*args, **kwargs)
                except TailRecurseException, e:
                    args = e.args
                    kwargs = e.kwargs
    func.__doc__ = g.__doc__
    return func
@tail_call_optimized
def factorial(n, acc=1):
    "calculate a factorial"
    if n == 0:
        return acc
    return factorial(n-1, n*acc)
print factorial(10000)

這里解釋一下sys._getframe()函數(shù):

sys._getframe([depth]):
Return a frame object from the call stack.
If optional integer depth is given, return the frame object that many calls below the top of the stack.
If that is deeper than the call stack, ValueEfror is raised. The default for depth is zero,
returning the frame at the top of the call stack.

即返回depth深度調(diào)用的棧幀對象.

import sys
def get_cur_info():
    print sys._getframe().f_code.co_filename  # 當(dāng)前文件名
    print sys._getframe().f_code.co_name  # 當(dāng)前函數(shù)名
    print sys._getframe().f_lineno # 當(dāng)前行號
    print sys._getframe().f_back # 調(diào)用者的幀

更多關(guān)于sys._getframe的使用請看http://www.kemok4.com/article/181387.htm

說一下tail_call_optimized實(shí)現(xiàn)尾遞歸優(yōu)化的原理:

當(dāng)遞歸函數(shù)被該裝飾器修飾后, 遞歸調(diào)用在裝飾器while循環(huán)內(nèi)部進(jìn)行, 每當(dāng)產(chǎn)生新的遞歸調(diào)用棧幀時: f.f_back.f_back.f_code == f.f_code:, 就捕獲當(dāng)前尾調(diào)用函數(shù)的參數(shù), 并拋出異常, 從而銷毀遞歸棧并使用捕獲的參數(shù)手動調(diào)用遞歸函數(shù). 所以遞歸的過程中始終只存在一個棧幀對象, 達(dá)到優(yōu)化的目的.

為了更清晰的展示開啟尾遞歸優(yōu)化前、后調(diào)用棧的變化和tail_call_optimized裝飾器拋異常退出遞歸調(diào)用棧的作用, 我這里利用pudb調(diào)試工具做了動圖:

開啟尾遞歸優(yōu)化前的調(diào)用棧

Python怎么開啟尾遞歸優(yōu)化

開啟尾遞歸優(yōu)化后(tail_call_optimized裝飾器)的調(diào)用棧

Python怎么開啟尾遞歸優(yōu)化

通過pudb右邊欄的stack, 可以很清晰的看到調(diào)用棧的變化.

感謝各位的閱讀,以上就是“Python怎么開啟尾遞歸優(yōu)化”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對Python怎么開啟尾遞歸優(yōu)化這一問題有了更深刻的體會,具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識點(diǎn)的文章,歡迎關(guān)注!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI