[譯]讓Python程序輕鬆加速的方法

做者:Cameron MacLeodhtml

翻譯:老齊前端


最近,我讀了一篇有趣的文章,文中介紹了一些未充分使用的Python特性的。在文章中,做者提到,從Python 3.2開始,標準庫附帶了一個內置的裝飾器functools.lru_cache。我發現這個裝飾器很使人興奮,有了它,咱們有可能輕鬆地爲許多應用程序加速。python

你可能在想,這很好,但這個裝飾器到底是什麼?它提供對已構建的緩存的訪問,該緩存使用LRU(譯者注: Least Recently Used的縮寫,即最近最少使用,是一種經常使用的頁面置換算法,選擇最近最久未使用的頁面予以淘汰。)的置換策略,所以被命名爲lru_cache。固然,這句話聽起來可能有點使人膽怯,因此讓咱們把它分解一下。web

什麼是緩存?

緩存是一個能夠快速訪問的地方,能夠在它裏面存儲訪問速度較慢的內容。爲了演示這一點,讓咱們以你的web瀏覽器爲例。算法

從網絡上讀取網頁可能須要幾秒鐘,即便是快速的網絡鏈接也如此。在計算機時代,這個問題是永恆的。爲了解決這個問題,瀏覽器將你已經訪問過的網頁存儲在計算機的緩存中,這樣訪問速度會加快數千倍。數據庫

使用緩存下載網頁的步驟以下:json

  1. 檢查頁面的本地緩存。若是頁面在那裏,返回該頁面。
  2. 在因網上找到網頁並從那裏下載。
  3. 將該網頁存儲在緩存中,以便未來更快地訪問。

雖然緩存並不會讓你第一次訪問網頁的速度加快,但一般你是要多次訪問某一個網站頁面的(想一想Facebook——注:對多數國人來說,可能不是這個網站,或者你的電子郵件),有了緩存以後,之後每次訪問都會更快。flask

瀏覽器並非惟一使用緩存的,從服務器到CPU和硬盤或SSD之間的計算機硬件,它們無處不在。從緩存中能夠很快地獲取數據,所以當你不止一次獲取數據時,它能夠大大加快程序的速度。瀏覽器

LRU是什麼意思?

緩存只能存儲有限數量的東西,並且一般它比可能存入所緩存的東西要小得多(例如,你的硬盤比互聯網小得多)。這意味着有時須要將緩存中已有內容替換掉,放入其餘內容。對於去掉什麼的決策方法被稱爲置換策略。緩存

這就是LRU的用武之地。LRU表明最近用得最少的緩存中內容,這是一種經常使用的緩存置換策略。

爲何置換策略很重要?

「最近使用最少」這種置換策略的基本思想是:若是你有一段時間沒有訪問過某個東西,你可能近期不會訪問它。要使用此策略,只需在緩存已滿時刪除最先使用的項便可。

在上圖中,緩存中的每一個項都附帶了訪問時間。依據LRU策略,選擇訪問時間爲2:55PM 的項做爲要置換的項,由於它是最先被訪問的。若是有兩個對象具備相同的訪問時間,那麼LRU將從中隨機選擇一個。

這種去掉長時間不用的東西的策略,被稱爲Bélády的最優算法,它是置換緩存內容的最佳策略。固然,咱們根本不知道將來會有什麼操做。謝天謝地,在許多狀況下,LRU提供了近乎最佳的性能。

怎樣使用它?

functools.lru_cache是一個裝飾器,所以你能夠將它放在函數的頂部:

import functools

@functools.lru_cache(maxsize=128)
def fib(n):
  if n < 2:
    return 1
  return fib(n-1) + fib(n-2)
複製代碼

Fibonacci數列在遞歸示例中常常被用到,要提高這個函數的速度,使用functools.lru_cache以後,不費吹灰之力,就能讓這個遞歸函數狂飆。在個人機器上運行這些代碼,獲得了這個函數有緩存版本和沒有緩存版本的如下結果。

$ python3 -m timeit -s 'from fib_test import fib' 'fib(30)'
10 loops, best of 3: 282 msec per loop

$ python3 -m timeit -s 'from fib_test import fib_cache' 'fib_cache(30)'
10000000 loops, best of 3: 0.0791 usec per loop
複製代碼

增長一行代碼以後,速度提升了3565107倍。

固然,我認爲很難看出你在實際中會如何使用它,由於咱們不多須要計算斐波那契數列。回到web頁面示例,咱們能夠舉一個更實際的用緩存渲染前端模板的例子。

在服務器開發中,一般單個頁面存儲爲具備佔位符變量的模板。例如,下面是一個頁面模板,該頁面顯示某一天各類足球比賽的結果。

<html>
  <body>
    <h1>Matches for {{day}}</h1>
    <table id="matches">
      <tr>
        <td>Home team</td>
        <td>Away team</td>
        <td>Score</td>
      </tr>
      {% for match in matches %}
      <tr>
        <td>{{match["home"]}}</td>
        <td>{{match["away"]}}</td>
        <td>{{match["home_goals"]}} - {{match["away_goals"]}}</td>
      </tr>
      {% endfor %}
    </table>
  </body>
</html>
複製代碼

呈現模板時,看起來以下所示:

這是緩存的主要目標,由於天天的結果不會改變,並且極可能天天會有屢次訪問。下面是一個提供此模板的Flask應用程序。我引入了50ms的延遲來模擬經過網絡或者從大型數據庫獲取匹配字典。

import json
import time
from flask import Flask, render_template

app = Flask(__name__)

with open('match.json','r') as f:
    match_dict = json.load(f)

def get_matches(day):
    # simulate network/database delay
    time.sleep(0.05)
    return match_dict[day]

@app.route('/matches/<day>')
def show_matches(day):
    matches = get_matches(day)
    return render_template('matches.html', matches=matches, day=day)

if __name__ == "__main__":
    app.run()
複製代碼

使用requests在不緩存的狀況下得到三天的數據,在個人計算機上本地運行平均須要171ms。這還不錯,但咱們能夠作得更好,即便考慮到人爲的延遲。

@app.route('/matches/<day>')
@functools.lru_cache(maxsize=4)
def show_matches(day):
    matches = get_matches(day)
    return render_template('matches.html', matches=matches, day=day)
複製代碼

在本例中,我設置了maxsize=4,由於個人測試腳本只有相同的三天,最好設置2次冪。使用這種方法,10個循環的平均速度能夠降到13.7ms。

還有什麼應該知道?

Python文檔雖然很詳細,可是有一些東西仍是要強調的。

內置函數

裝飾器附帶了一些頗有用的內置函數。cache_info()返回訪問數(hits)、未訪問數(misses)和當前緩存使用量(currsize)、最大容量(maxsize),幫助你瞭解緩存使用狀況。cache_clear()將刪除緩存中的全部元素。

有時候不要使用緩存

一般,只有在如下狀況下才能使用緩存:

  • 在緩存期內,數據不會更改。
  • 函數將始終爲相同的參數返回相同的值(所以時間和隨機對緩存沒有意義)。
  • 函數沒有反作用。若是緩存被訪問,則永遠不會調用該函數,所以請確保不更改其中的任何狀態。
  • 函數不返回不一樣的可變對象。例如,返回列表的函數不適合緩存,由於將要緩存的是對列表的引用,而不是列表內容。

原文連接:www.cameronmacleod.com/blog/python…

關注微信公衆號:老齊教室。讀深度文章,得精湛技藝,享絢麗人生。

相關文章
相關標籤/搜索