搜尋
首頁後端開發Python教學Python中的@cache怎麼用

    Python中的@cache有什麼妙用?

    透過採用快取策略,可以將空間轉換為時間,進而提升電腦系統效能。快取在程式碼中的作用是優化程式碼的運行速度,儘管會增加記憶體佔用。

    在Python的內建模組 functools 中,提供了高階函數 cache() 用於實作緩存,以裝飾器的方式使用: @cache。

    @cache快取功能介紹

    在cache的原始碼中,對cache的描述是:Simple lightweight unbounded cache. Sometimes called “memoize”. 翻譯成中文:簡單的輕量級無限制緩存。有時也被稱為「記憶化」。

    def cache(user_function, /):
        'Simple lightweight unbounded cache.  Sometimes called "memoize".'
        return lru_cache(maxsize=None)(user_function)

    cache() 的程式碼只有一行,呼叫了 lru_cache() 函數,傳入一個參數 maxsize=None。 lru_cache() 也是functools 模組中的函數,查看lru_cache() 的源碼,maxsize 的預設值是128,表示最大快取128個數據,如果數據超過了128個,則按LRU(最久未使用)刪除多的演算法數據。 cache()將maxsize設定成None,則 LRU 特性被停用且快取數量可以無限成長,所以稱為「unbounded cache」(無限制快取)。

    lru_cache() 使用了 LRU(Least Recently Used)最久未使用演算法,這也是函數名稱中有 lru 三個字母的原因。最久未使用演算法的機制是,假設一個資料在最近一段時間沒有被存取到,那麼在將來它被存取的可能性也很小, LRU演算法選擇將最近最少使用的資料淘汰,保留那些經常被使用的數據。

    cache() 是在Python3.9版本新增的,lru_cache() 是在Python3.2版本新增的, cache() 在lru_cache() 的基礎上取消了快取數量的限制,其實跟技術進步、硬體效能的大幅提升有關,cache() 和lru_cache() 只是同一個功能的不同版本。

    lru_cache() 基本上是一個為函數提供快取功能的裝飾器,快取maxsize 群組傳入參數,下次以相同參數呼叫函數時直接返回上一次的結果,用以節約高開銷或高I/O函數的呼叫時間。

    @cache的應用場景

    快取的應用程式場景很廣泛,如靜態 Web 內容的緩存,可以直接在使用者存取靜態網頁的函數上加 @cache 裝飾器。

    在某些遞歸的程式碼中,有重複傳入同一個參數執行函數程式碼的情況,使用快取可以避免重複計算,降低程式碼的時間複雜度。

    接下來,我用斐波那契數列作為例子來說明 @cache 的作用,如果前面的內容你看完了還一知半解,相信看完例子你會茅塞頓開。

    斐波那契數列是指這樣一個數列:1、1、2、3、5、8、13、21、34、… ,從第三個數開始,每個數都是前兩個數之和。大多數初學者都曾經編寫過斐波那契數列的程式碼,它的實作並不困難,在Python中,程式碼非常簡潔。如下:

    def feibo(n):
        # 第0个数和第1个数为1
        a, b = 1, 1
        for _ in range(n):
            # 将b赋值给a,将a+b赋值给b,循环n次
            a, b = b, a+b
        return a

    當然,斐波那契數列的程式碼實作方式有很多種(至少五、六種),本文為了說明@cache 的應用場景,用遞歸的方式來寫斐波那契數列的代碼。如下:

    def feibo_recur(n):
        if n < 0:
            return "n小于0无意义"
        # n为0或1时返回1(前两个数为1)
        if n == 0 or n == 1:
            return 1
        # 根据斐波那契数列的定义,其他情况递归返回前两个数之和
        return feibo_recur(n-1) + feibo_recur(n-2)

    遞迴程式碼執行時會一直遞歸到feibo_recur(1)和feibo_recur(0),如下圖所示(以求第6個數為例)。

    Python中的@cache怎麼用

    求F(5)時要先求F(4)和F(3),求F(4)時要先求F(3)和F( 2),… 以此類推,遞歸的過程與二元樹深度優先遍歷的過程類似。已知高度為k 的二元樹最多可以有2k-1 個節點,根據上面遞歸調用的圖示,二叉樹的高度是n,節點最多為2n-1, 也就是遞歸呼叫函數的次數最多為2n-1 次,所以遞歸的時間複雜度為O(2^n) 。

    時間複雜度為O(2^n)時,執行時間隨 n 的增大變化非常誇張,以下實際測試一下。

    import time
    for i in [10, 20, 30, 40]:
        start = time.time()
        print(f&#39;第{i}个斐波那契数:&#39;, feibo_recur(i))
        end = time.time()
        print(f&#39;n={i} Cost Time: &#39;, end - start)

    Output:

    第10個斐波那契數: 89
    n=10 Cost Time:  0.0
    第20個斐波那契數: 10946
    n=20 Cost Time:  0.0015988349914550781
    第30個斐波那契數: 1346269
    n=30 Cost Time:  0.170512914657512914657512916575129146575129112506221222012240 n=40 Cost Time:  20.90010976791382

    從運行時間可以看出,在n 很小時,運行很快,隨著n 的增大,運行時間極速上升,尤其n 逐步增加到30和40時,運行時間變化得特別明顯。為了更清楚看出時間變化規律,再進一步進行測試。
    for i in [41, 42, 43]:
        start = time.time()
        print(f&#39;第{i}个斐波那契数:&#39;, feibo_recur(i))
        end = time.time()
        print(f&#39;n={i} Cost Time: &#39;, end - start)

    Output:

    第41個斐波那契數: 267914296
    n=41 Cost Time:  33.77224683761597

    第42個斐波那契數: 433494437
    n=42 Cost Time:  55.86398696899414
    第43個斐波那契數: 701408733
    n=43 Cost Time:  92.55108690261841

    从上面的变化可以看到,时间是指数级增长的(大约按1.65的指数增长),这跟时间复杂度为 O(2^n) 相符。按照这个时间复杂度,假如要计算第50个斐波那契数列,差不多要等一个小时,非常不合理,也说明递归的实现方式运算量过大,存在明显的不足。如何解决这种不足,降低运算量呢?接下来看如何进行优化。

    根据前面的分析,递归代码运算量大,是因为递归执行时会不断的计算 feibo_recur(n-1) 和 feibo_recur(n-2),如示例图中,要得到 feibo_recur(5) ,feibo_recur(1) 调用了5次。随着 n 的增大,调用次数呈指数级增长,导致出现大量的重复操作,浪费了许多时间。

    Python中的@cache怎麼用

    假如有一个地方将每个 n 的执行结果记录下来,当作“备忘录”,下次函数再接收到这个相同的参数时,直接从备忘录中获取结果,而不用去执行递归的过程,就可以避免这些重复调用。在 Python 中,可以创建一个字典或列表来当作“备忘录”使用。

    temp = {}  # 创建一个空字典,用来记录第i个斐波那契数列的值
    def feibo_recur_temp(n):
        if n < 0:
            return "n小于0无意义"
        # n为0或1时返回1(前两个数为1)
        if n == 0 or n == 1:
            return 1
        if n in temp:  # 如果temp字典中有n,则直接返回值,不调用递归代码
            return temp[n]
        else:
            # 如果字典中还没有第n个斐波那契数,则递归计算并保存到字典中
            temp[n] = feibo_recur_temp(n-1) + feibo_recur_temp(n-2)
            return temp[n]

    上面的代码中,创建了一个空字典用于存放每个 n 的执行结果。每次调用函数,都先查看字典中是否有记录,如果有记录就直接返回,没有记录就递归执行并将结果记录到字典中,再从字典中返回结果。这里的递归其实都只执行了一次计算,并没有真正的递归,如第一次传入 n 等于 5,执行 feibo_recur_temp(5),会递归执行 n 等于 4, 3, 2, 1, 0 的情况,每个 n 计算过一次后 temp 中都有了记录,后面都是直接到 temp 中取数相加。每个 n 都是从temp中取 n-1 和 n-2 的值来相加,执行一次计算,所以时间复杂度是 O(n) 。

    下面看一下代码的运行时间。

    for i in [10, 20, 30, 40, 41, 42, 43]:
        start = time.time()
        print(f&#39;第{i}个斐波那契数:&#39;, feibo_recur_temp(i))
        end = time.time()
        print(f&#39;n={i} Cost Time: &#39;, end - start)
    print(temp)

    Output:

    第10个斐波那契数: 89
    n=10 Cost Time:  0.0
    第20个斐波那契数: 10946
    n=20 Cost Time:  0.0
    第30个斐波那契数: 1346269
    n=30 Cost Time:  0.0
    第40个斐波那契数: 165580141
    n=40 Cost Time:  0.0
    第41个斐波那契数: 267914296
    n=41 Cost Time:  0.0
    第42个斐波那契数: 433494437
    n=42 Cost Time:  0.0
    第43个斐波那契数: 701408733
    n=43 Cost Time:  0.0
    {2: 2, 3: 3, 4: 5, 5: 8, 6: 13, 7: 21, 8: 34, 9: 55, 10: 89, 11: 144, 12: 233, 13: 377, 14: 610, 15: 987, 16: 1597, 17: 2584, 18: 4181, 19: 6765, 20: 10946, 21: 17711, 22: 28657, 23: 46368, 24: 75025, 25: 121393, 26: 196418, 27: 317811, 28: 514229, 29: 832040, 30: 1346269, 31: 2178309, 32: 3524578, 33: 5702887, 34: 9227465, 35: 14930352, 36: 24157817, 37: 39088169, 38: 63245986, 39: 102334155, 40: 165580141, 41: 267914296, 42: 433494437, 43: 701408733}

    可以观察到,代码的运行时间已经减少到小数点后很多位了(时间过短,只显示了0.0)。然而,temp 字典存储了每个数字的斐波那契数,这需要使用额外的内存空间,以换取更高的时间效率。

    上面的代码也可以用列表来当“备忘录”,代码如下。

    temp = [1, 1]
    def feibo_recur_temp(n):
        if n < 0:
            return "n小于0无意义"
        if n == 0 or n == 1:
            return 1
        if n < len(temp):
            return temp[n]
        else:
            # 第一次执行时,将结果保存到列表中,后续直接从列表中取
            temp.append(feibo_recur_temp(n-1) + feibo_recur_temp(n-2))
            return temp[n]

    现在,已经剖析了递归代码重复执行带来的时间复杂度问题,也给出了优化时间复杂度的方法,让我们将注意力转回到本文介绍的 @cache 装饰器。@cache 装饰器的作用是将函数的执行结果缓存,在下次以相同参数调用函数时直接返回上一次的结果,与上面的优化方式完全一致。

    所以,只需要在递归函数上加 @cache 装饰器,递归的重复执行就可以解决,时间复杂度就能从 O(2^n) 降为 O(n) 。代码如下:

    from functools import cache
    @cache
    def feibo_recur(n):
        if n < 0:
            return "n小于0无意义"
        if n == 0 or n == 1:
            return 1
        return feibo_recur(n-1) + feibo_recur(n-2)

    使用 @cache 装饰器,可以让代码更简洁优雅,并且让你专注于处理业务逻辑,而不需要自己实现缓存。下面看一下实际的运行时间。

    for i in [10, 20, 30, 40, 41, 42, 43]:
        start = time.time()
        print(f&#39;第{i}个斐波那契数:&#39;, feibo_recur(i))
        end = time.time()
        print(f&#39;n={i} Cost Time: &#39;, end - start)

    Output:

    第10个斐波那契数: 89
    n=10 Cost Time:  0.0
    第20个斐波那契数: 10946
    n=20 Cost Time:  0.0
    第30个斐波那契数: 1346269
    n=30 Cost Time:  0.0
    第40个斐波那契数: 165580141
    n=40 Cost Time:  0.0
    第41个斐波那契数: 267914296
    n=41 Cost Time:  0.0
    第42个斐波那契数: 433494437
    n=42 Cost Time:  0.0
    第43个斐波那契数: 701408733
    n=43 Cost Time:  0.0

    完美地解决了问题,所有运行时间都被精确到了小数点后数位(即使只显示 0.0),非常巧妙。若今后遇到类似情形,可以直接采用 @cache 实现缓存功能,通过“记忆化”处理。

    补充:Python @cache装饰器

    @cache和@lru_cache(maxsize=None)可以用来寄存函数对已处理参数的结果,以便遇到相同参数可以直接给出答案。前者无限制存储数量,而后者通过设定maxsize限制存储数量的上限。

    例:

    @lru_cache(maxsize=None) # 等价于@cache
    def test(a,b):
        print(&#39;开始计算a+b的值...&#39;)
        return a + b

    可以用来做某些递归、动态规划。比如斐波那契数列的各项值从小到大输出。其实类似用数组保存前项的结果,都需要额外的空间。不过用装饰器可以省略额外空间代码,减少了出错的风险。

    以上是Python中的@cache怎麼用的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    陳述
    本文轉載於:亿速云。如有侵權,請聯絡admin@php.cn刪除
    详细讲解Python之Seaborn(数据可视化)详细讲解Python之Seaborn(数据可视化)Apr 21, 2022 pm 06:08 PM

    本篇文章给大家带来了关于Python的相关知识,其中主要介绍了关于Seaborn的相关问题,包括了数据可视化处理的散点图、折线图、条形图等等内容,下面一起来看一下,希望对大家有帮助。

    详细了解Python进程池与进程锁详细了解Python进程池与进程锁May 10, 2022 pm 06:11 PM

    本篇文章给大家带来了关于Python的相关知识,其中主要介绍了关于进程池与进程锁的相关问题,包括进程池的创建模块,进程池函数等等内容,下面一起来看一下,希望对大家有帮助。

    Python自动化实践之筛选简历Python自动化实践之筛选简历Jun 07, 2022 pm 06:59 PM

    本篇文章给大家带来了关于Python的相关知识,其中主要介绍了关于简历筛选的相关问题,包括了定义 ReadDoc 类用以读取 word 文件以及定义 search_word 函数用以筛选的相关内容,下面一起来看一下,希望对大家有帮助。

    归纳总结Python标准库归纳总结Python标准库May 03, 2022 am 09:00 AM

    本篇文章给大家带来了关于Python的相关知识,其中主要介绍了关于标准库总结的相关问题,下面一起来看一下,希望对大家有帮助。

    Python数据类型详解之字符串、数字Python数据类型详解之字符串、数字Apr 27, 2022 pm 07:27 PM

    本篇文章给大家带来了关于Python的相关知识,其中主要介绍了关于数据类型之字符串、数字的相关问题,下面一起来看一下,希望对大家有帮助。

    分享10款高效的VSCode插件,总有一款能够惊艳到你!!分享10款高效的VSCode插件,总有一款能够惊艳到你!!Mar 09, 2021 am 10:15 AM

    VS Code的确是一款非常热门、有强大用户基础的一款开发工具。本文给大家介绍一下10款高效、好用的插件,能够让原本单薄的VS Code如虎添翼,开发效率顿时提升到一个新的阶段。

    详细介绍python的numpy模块详细介绍python的numpy模块May 19, 2022 am 11:43 AM

    本篇文章给大家带来了关于Python的相关知识,其中主要介绍了关于numpy模块的相关问题,Numpy是Numerical Python extensions的缩写,字面意思是Python数值计算扩展,下面一起来看一下,希望对大家有帮助。

    python中文是什么意思python中文是什么意思Jun 24, 2019 pm 02:22 PM

    pythn的中文意思是巨蟒、蟒蛇。1989年圣诞节期间,Guido van Rossum在家闲的没事干,为了跟朋友庆祝圣诞节,决定发明一种全新的脚本语言。他很喜欢一个肥皂剧叫Monty Python,所以便把这门语言叫做python。

    See all articles

    熱AI工具

    Undresser.AI Undress

    Undresser.AI Undress

    人工智慧驅動的應用程序,用於創建逼真的裸體照片

    AI Clothes Remover

    AI Clothes Remover

    用於從照片中去除衣服的線上人工智慧工具。

    Undress AI Tool

    Undress AI Tool

    免費脫衣圖片

    Clothoff.io

    Clothoff.io

    AI脫衣器

    AI Hentai Generator

    AI Hentai Generator

    免費產生 AI 無盡。

    熱門文章

    R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
    3 週前By尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O.最佳圖形設置
    3 週前By尊渡假赌尊渡假赌尊渡假赌
    R.E.P.O.如果您聽不到任何人,如何修復音頻
    3 週前By尊渡假赌尊渡假赌尊渡假赌

    熱工具

    SublimeText3 Mac版

    SublimeText3 Mac版

    神級程式碼編輯軟體(SublimeText3)

    PhpStorm Mac 版本

    PhpStorm Mac 版本

    最新(2018.2.1 )專業的PHP整合開發工具

    Atom編輯器mac版下載

    Atom編輯器mac版下載

    最受歡迎的的開源編輯器

    mPDF

    mPDF

    mPDF是一個PHP庫,可以從UTF-8編碼的HTML產生PDF檔案。原作者Ian Back編寫mPDF以從他的網站上「即時」輸出PDF文件,並處理不同的語言。與原始腳本如HTML2FPDF相比,它的速度較慢,並且在使用Unicode字體時產生的檔案較大,但支援CSS樣式等,並進行了大量增強。支援幾乎所有語言,包括RTL(阿拉伯語和希伯來語)和CJK(中日韓)。支援嵌套的區塊級元素(如P、DIV),

    Dreamweaver Mac版

    Dreamweaver Mac版

    視覺化網頁開發工具