有效提升Python程式碼效能的三個層面

金色旭光發表於2022-03-12

使用python進入一個熟練的狀態之後就會思考提升程式碼的效能,尤其是python的執行效率還有很大提升空間(委婉的說法)。面對提升效率這個話題,python自身提供了很多高效能模組,很多大牛開發出了高效第三方包,可謂是百花齊放。下面根據我個人使用總結出提升效能的幾個層面和相關方法。

python程式碼優化:

  1. 語法層面
  2. 高效模組
  3. 直譯器層面

語法層面

  1. 變數定義
  2. 資料型別
  3. 條件判斷
  4. 迴圈
  5. 生成器

變數定義

  1. 多使用區域性變數少使用全域性變數,名稱空間中區域性變數優先搜尋

條件判斷

  1. 可以使用字典的key value特性,直接用key命中條件,避免if判斷
  2. 用in操作替換if else判斷
  3. 使用any 或 all 將多個判斷一起處理,減少if else的分支
  4. if條件的短路特性。if a or b這種判斷中,如果a是True就不會判斷b,所以將True條件寫在前面可以節省判斷時間。同理 and 判斷將假寫在前面,後面一個條件不判斷

資料型別

  1. 使用dict 或set查詢,替換list或tuple
  2. 集合的交併補差操作效率非常高。for迴圈和集合都可以處理的選擇集合解決,集合的效率遠高於迴圈

迴圈

  1. 用for迴圈代替while迴圈,for迴圈比while迴圈快
  2. 使用隱式for迴圈代替顯式for迴圈。如sum,map,filter,reduce等都是隱式for迴圈。隱式迴圈快於顯式迴圈
  3. 儘量不要打斷迴圈。打斷迴圈的放在外面。有判斷條件的語句和與迴圈不相關的操作語句儘量放在for外面
  4. 應當將最長的迴圈放在最內層,最短的迴圈放在最外層,以減少CPU跨切迴圈層的次數
  5. 使用生成式替換迴圈建立

合理使用迭代器和生成器

需要迭代出大量資料的場景,不需要將所有資料建立出來,合理使用生成器減少記憶體消耗

items_gen = (i for i in range(5000))
>>> items_gen.__sizeof__()
96
items_list = [i for i in ragne(5000)]
>>> items_list.__sizeof__()
43016

高效模組

  1. collections 資料增強模組
  2. itertools 高效迭代模組
  3. array 高效陣列
  4. functool 用於處理函式的高階函式包

collections

  1. Counter: 高效的統計庫
  2. defaultdict:帶預設值的字典
  3. ChainMap:高效組合字典的庫
  4. deque: 雙端佇列,高效插入刪除

詳細使用參見另一篇專門講collections的文章 Python原生資料結構增強模組collections

itertools

  1. chain:多個可迭代物件構建成一個新的可迭代物件
  2. groupby:按照指定的條件分類,輸出條件和符合條件的元素
  3. from_iteratorable:一個迭代物件中將所有元素類似於chain一樣,統一返回
  4. islice:對迭代器進行切片,能指定start和stop以及步長

詳細使用參見另一篇專門講itertools的文章Python高效能工具迭代標準庫itertools

array

array 模組是python中實現的一種高效的陣列儲存型別。
它和list相似,但是所有的陣列成員必須是同一種型別,在建立陣列的時候,就確定了陣列的型別。

functool

functools.lru_cache 對函式做快取

lru_cache 是一個裝飾器,為函式提供快取功能。被裝飾的函式以相同引數呼叫時直接返回上一次的結果。
不做快取

import time
 
def fibonacci(n):
    """斐波那契函式"""
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


start = time.time()
res = fibonacci(40)
end = time.time()
print(res)
print(end - start)
102334155
32.14816737174988

做快取

import time
from functools import lru_cache      
 
@lru_cache
def fibonacci(n):
    """斐波那契函式"""
    if n < 2:
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


start = time.time()
res = fibonacci(40)
end = time.time()
print(res)
print(end - start)

102334155
0.00020623207092285156

使用注意:

  1. 快取是按照引數作為鍵。呼叫函式時任意一個引數發生變化都不會返回之前快取結果
  2. 所有引數必須可雜湊hash。也就是說引數只能是不可變物件

直譯器層面:

減少python執行過程

python 程式碼的執行過程為:

  1. 編譯器將原始碼編譯成中間狀態的位元組碼
  2. 直譯器執行位元組碼,將位元組碼轉成機器碼在cpu上執行

python慢的原因主要是因為直譯器。解決辦法有兩個:
一是解決辦法是使用C/C++語言重寫Python函式,但是這要求程式設計師對C/C++語言熟悉,且除錯速度慢,不適合絕大多數Python程式設計師。
另外一種非常方便快捷的解決辦法就是使用Just-In-Time(JIT)技術。

Just-In-Time(JIT)技術為解釋語言提供了一種優化,它能克服上述效率問題,極大提升程式碼執行速度,同時保留Python語言的易用性。使用JIT技術時,JIT編譯器將Python原始碼編譯成機器直接可以執行的機器語言,並可以直接在CPU等硬體上執行。這樣就跳過了原來的虛擬機器,執行速度幾乎與用C語言程式設計速度並無二致。

Numba是一個針對Python的開源JIT編譯器,由Anaconda公司主導開發,可以對Python原生程式碼進行CPU和GPU加速。

import time

def fun(x):
    total = 0
    start = time.time()
    for i in range(1,x+1):
        total += i 
    end = time.time()
    print(total)
    print(end - start)

fun(100000000)
5000000050000000
5.934630393981934
import time
from numba import jit, int32

@jit(int32(int32))
def fun(x):
    total = 0
    start = time.time()
    for i in range(1,x+1):
        total += i 
    end = time.time()
    print(total)
    print(end - start)

fun(100000000)
5000000050000000
0.1186532974243164

速度有60倍提升

相關文章