最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Python循環(huán)優(yōu)化指南:從10秒到0.1秒的性能調(diào)優(yōu)

 更新時(shí)間:2026年03月26日 11:37:10   作者:站大爺IP  
Python寫起來快,跑起來慢,這是共識(shí),尤其是循環(huán),簡直就是Python的性能黑洞,但很多人不知道的是,一個(gè)寫得不講究的循環(huán)和經(jīng)過優(yōu)化的循環(huán),性能差距可以達(dá)到幾十倍甚至上百倍,下面小編就和大家分享幾個(gè)Python循環(huán)優(yōu)化技巧吧

小李是個(gè)剛?cè)胄械臄?shù)據(jù)分析師,今天接了個(gè)活兒——處理一份三百萬行的用戶行為日志。他的代碼寫得很清爽,一個(gè)for循環(huán)套著幾個(gè)if判斷,逐行讀取、逐行處理、逐行寫入。邏輯沒問題,結(jié)果也正確,就是跑起來有點(diǎn)慢。

他泡了杯咖啡,代碼開始跑??Х群韧炅?,進(jìn)度條才走了5%。他算了算,按這個(gè)速度,跑完要將近四十分鐘。這還只是一天的數(shù)據(jù),明天還有新的,后天也有。小李盯著屏幕,陷入了沉思——代碼邏輯沒錯(cuò),但就是慢,這該怎么辦?

這個(gè)故事在程序員圈子里每天都在上演。Python寫起來快,跑起來慢,這是共識(shí)。尤其是循環(huán),簡直就是Python的性能黑洞。但很多人不知道的是,一個(gè)寫得不講究的循環(huán)和經(jīng)過優(yōu)化的循環(huán),性能差距可以達(dá)到幾十倍甚至上百倍。

那個(gè)跑了10秒的循環(huán)長什么樣

我們先看看小李最初那段代碼大概是什么樣子的。

假設(shè)他要處理三百萬條數(shù)據(jù),每條數(shù)據(jù)是一串用逗號(hào)分隔的字符串,包含用戶ID、時(shí)間戳、行為類型、金額等字段。他要做的事情是:篩選出金額大于100的交易,把時(shí)間戳轉(zhuǎn)換成可讀的日期格式,然后寫入一個(gè)新列表。

import time
import random

# 模擬三百萬條數(shù)據(jù)
data = []
for i in range(3000000):
    line = f"{i},2024-01-01 12:00:00,click,{random.randint(1, 500)}"
    data.append(line)

start = time.time()

result = []
for line in data:
    parts = line.split(',')
    user_id = parts[0]
    timestamp = parts[1]
    action = parts[2]
    amount = int(parts[3])
    
    if amount > 100:
        # 時(shí)間戳轉(zhuǎn)換的模擬操作
        formatted_time = timestamp.replace('-', '/')
        result.append(f"{user_id},{formatted_time},{action},{amount}")

end = time.time()
print(f"耗時(shí): {end - start:.2f}秒")

這段代碼在普通的機(jī)器上跑三百萬條數(shù)據(jù),大概需要10秒左右。看起來不算太慢?但想想看,如果數(shù)據(jù)量是三個(gè)億,那就是100秒。如果邏輯更復(fù)雜,可能幾百秒。關(guān)鍵是,這種慢是可以被優(yōu)化掉的。

病根在哪里

要治病,得先找到病根。Python循環(huán)慢,有幾個(gè)核心原因。

第一層原因是Python本身就是解釋型語言。每一行代碼在執(zhí)行時(shí),解釋器要做大量的工作——解析語法、查找變量、分配內(nèi)存、調(diào)用函數(shù)。循環(huán)體里每多寫一行代碼,這些操作就要重復(fù)執(zhí)行幾百萬次。累加起來,就是肉眼可見的延遲。

第二層原因是屬性查找。在循環(huán)里寫line.split(','),Python每次都要去line這個(gè)對(duì)象里找split方法在哪里。三百萬次循環(huán),就是三百萬次屬性查找。同樣的道理,parts[0]、parts[1]這些索引訪問,每次也要做類型檢查。

第三層原因是動(dòng)態(tài)類型。Python的變量沒有類型聲明,每次運(yùn)行時(shí)都要推斷類型。amount = int(parts[3])這行,Python要先確定parts[3]是個(gè)字符串,然后調(diào)用整數(shù)轉(zhuǎn)換函數(shù),再檢查轉(zhuǎn)換結(jié)果是不是整數(shù)。這些動(dòng)態(tài)檢查在三百萬次循環(huán)里,成本相當(dāng)可觀。

第一刀:減少循環(huán)體里的操作

優(yōu)化的第一條原則是:循環(huán)體里能少做的事,絕不多做。

看看上面那段代碼,user_id、timestampaction這三個(gè)變量,在篩選之后只用了一次,卻每次都定義出來。完全可以在篩選通過之后再取用。

result = []
for line in data:
    parts = line.split(',')
    amount = int(parts[3])
    
    if amount > 100:
        formatted_time = parts[1].replace('-', '/')
        result.append(f"{parts[0]},{formatted_time},{parts[2]},{amount}")

這樣改完,三百萬次循環(huán)少做了幾百萬次變量賦值。能快多少?大概能快個(gè)1秒左右。這只是個(gè)開始。

第二刀:把屬性查找挪到循環(huán)外面

Python里每次寫line.split,解釋器都要去line對(duì)象的類里找split這個(gè)屬性。有個(gè)小技巧可以解決這個(gè)問題——在循環(huán)外面把方法賦值給一個(gè)局部變量。

result = []
split_method = str.split  # 直接把split方法拿出來
int_convert = int
for line in data:
    parts = split_method(line, ',')
    amount = int_convert(parts[3])
    
    if amount > 100:
        formatted_time = parts[1].replace('-', '/')
        result.append(f"{parts[0]},{formatted_time},{parts[2]},{amount}")

這樣做的好處是,循環(huán)體內(nèi)不再需要做屬性查找。Python直接拿著已經(jīng)找到的方法去調(diào)用。三百萬次循環(huán)下來,這個(gè)改動(dòng)能省下1到2秒。

第三刀:用列表推導(dǎo)式替代顯式循環(huán)

Python的列表推導(dǎo)式(list comprehension)是用C語言層面實(shí)現(xiàn)的,比Python層面的顯式循環(huán)快得多。當(dāng)你的循環(huán)只是為了構(gòu)建一個(gè)新列表時(shí),列表推導(dǎo)式是最佳選擇。

但這里有個(gè)問題——我們的循環(huán)里有篩選條件。好消息是,列表推導(dǎo)式也支持條件判斷。

def process_line(line):
    parts = line.split(',')
    amount = int(parts[3])
    if amount > 100:
        formatted_time = parts[1].replace('-', '/')
        return f"{parts[0]},{formatted_time},{parts[2]},{amount}"
    return None

result = [item for item in (process_line(line) for line in data) if item is not None]

這段代碼用了生成器表達(dá)式加列表推導(dǎo)式的組合。生成器表達(dá)式逐行處理,列表推導(dǎo)式收集非空的結(jié)果。把處理邏輯包進(jìn)一個(gè)函數(shù)里,雖然多了一次函數(shù)調(diào)用,但整體上因?yàn)榱斜硗茖?dǎo)式的底層優(yōu)化,速度反而會(huì)提升。

這樣改下來,原來的10秒能降到6秒左右。

第四刀:用map和filter組合

mapfilter也是用C實(shí)現(xiàn)的,比Python循環(huán)快??梢园烟幚砹鞒虒懗珊瘮?shù)鏈。

def parse_line(line):
    parts = line.split(',')
    return (parts[0], parts[1], parts[2], int(parts[3]))

def filter_by_amount(item):
    return item[3] > 100

def format_output(item):
    formatted_time = item[1].replace('-', '/')
    return f"{item[0]},{formatted_time},{item[2]},{item[3]}"

parsed = map(parse_line, data)
filtered = filter(filter_by_amount, parsed)
result = list(map(format_output, filtered))

這種寫法的好處是每個(gè)函數(shù)只做一件事,邏輯清晰,而且mapfilter的組合在性能上優(yōu)于顯式循環(huán)。跑下來大概能到4秒左右。

第五刀:避免重復(fù)的類型轉(zhuǎn)換

上面幾輪優(yōu)化下來,代碼已經(jīng)快了不少。但仔細(xì)觀察,parts[1].replace('-', '/')這一行,每次都在做字符串替換。如果數(shù)據(jù)量足夠大,字符串操作的成本會(huì)變得很明顯。

這里有一個(gè)小技巧——如果你知道時(shí)間戳的格式是固定的,可以用切片拼接的方式來替換,比調(diào)用replace快得多。

formatted_time = parts[1][:4] + '/' + parts[1][5:7] + '/' + parts[1][8:10]

這行代碼看起來很丑,但性能比replace好。因?yàn)樗蛔瞿J狡ヅ洌皇羌兇獾膬?nèi)存操作。三百萬次調(diào)用下來,這個(gè)改動(dòng)又能省下0.5秒。

同樣的思路,字符串拼接也有講究。用f-string已經(jīng)很快了,但如果需要拼的字段特別多,join方法在某些場景下會(huì)更穩(wěn)定。

第六刀:用內(nèi)置模塊分擔(dān)壓力

有些數(shù)據(jù)處理任務(wù),根本不應(yīng)該在Python循環(huán)里做。Python的內(nèi)置模塊itertoolscollections、operator提供了很多高性能的工具。

比如這個(gè)場景,如果用itertools.islice配合map,可以避免一次性把所有數(shù)據(jù)加載到內(nèi)存里。如果數(shù)據(jù)量巨大,這比直接用列表更友好。

更激進(jìn)的方案是換數(shù)據(jù)結(jié)構(gòu)。如果數(shù)據(jù)是結(jié)構(gòu)化的,可以考慮用pandas來處理。pandas的底層是C和NumPy,處理三百萬行數(shù)據(jù)只是眨眼間的事。

import pandas as pd

# 模擬數(shù)據(jù)
df = pd.DataFrame([line.split(',') for line in data], columns=['user_id', 'timestamp', 'action', 'amount'])
df['amount'] = df['amount'].astype(int)
df_filtered = df[df['amount'] > 100]
df_filtered['timestamp'] = df_filtered['timestamp'].str.replace('-', '/')
result = df_filtered.apply(lambda row: f"{row.user_id},{row.timestamp},{row.action},{row.amount}", axis=1).tolist()

這段代碼用pandas處理,三百萬行數(shù)據(jù)大概0.3到0.5秒就能跑完。為什么這么快?因?yàn)?code>pandas把循環(huán)推到了C層面,Python只是負(fù)責(zé)調(diào)用。

第七刀:把循環(huán)徹底干掉

最后一刀最狠——如果真的需要極致性能,就別在Python層面循環(huán)。

一種做法是把數(shù)據(jù)處理邏輯寫成SQL,讓數(shù)據(jù)庫去處理。數(shù)據(jù)庫的查詢優(yōu)化器比任何手寫的Python循環(huán)都聰明。

另一種做法是用Python的multiprocessing模塊做并行處理。把三百萬條數(shù)據(jù)切成八份,八個(gè)進(jìn)程同時(shí)跑,理想情況下耗時(shí)能降到原來的八分之一。但要注意,多進(jìn)程有額外的開銷,數(shù)據(jù)量不夠大的時(shí)候反而更慢。

更進(jìn)階的做法是用numba或者Cython把關(guān)鍵代碼編譯成機(jī)器碼。numba用起來很簡單,加一個(gè)裝飾器就能讓循環(huán)飛起來。

from numba import jit

@jit(nopython=True)
def process_data(data):
    # 注意:numba對(duì)Python對(duì)象的支持有限,需要把數(shù)據(jù)轉(zhuǎn)換成NumPy數(shù)組
    pass

但這條路有一定門檻,不適合所有場景。

小李的最終方案

小李后來沒選最極端的方案。他覺得代碼的可維護(hù)性也很重要,不能為了性能把代碼寫成天書。他最終選的是pandas方案——代碼簡潔,邏輯清晰,三百萬行數(shù)據(jù)從原來的10秒降到了0.4秒。

他算了一筆賬。如果每天跑一次,每次省9.6秒,一年下來省了將近一個(gè)小時(shí)??此撇欢啵P(guān)鍵是——他的代碼不再需要中途喝咖啡等了。點(diǎn)一下運(yùn)行,喝口水的時(shí)間,結(jié)果就出來了。

他把優(yōu)化前后的代碼都存了下來,在代碼注釋里寫了一句:“慢的版本留著做對(duì)比,提醒自己Python循環(huán)有多貴。”

性能優(yōu)化的心法

這幾刀砍下來,其實(shí)能總結(jié)出幾個(gè)通用的心法。

循環(huán)體越小越好。循環(huán)體里的每一行代碼,都會(huì)被放大幾百萬倍。能挪出去的,堅(jiān)決挪出去。能不用變量存的,就別存。

能用內(nèi)置的,就用內(nèi)置的。map、filter、列表推導(dǎo)式、itertools,這些都是C寫的,比Python循環(huán)快得多。Python的“內(nèi)置”兩個(gè)字,本身就是性能的保證。

數(shù)據(jù)量大的時(shí)候,換個(gè)工具。pandas、numpy、multiprocessing,這些工具存在的意義,就是幫你把循環(huán)從Python層面推出去。別死磕。

先寫對(duì),再寫快。優(yōu)化之前,先用一小段數(shù)據(jù)驗(yàn)證邏輯是否正確。在錯(cuò)誤的代碼上做優(yōu)化,是最大的浪費(fèi)時(shí)間。等邏輯穩(wěn)定了,再針對(duì)熱點(diǎn)部分下手。

用數(shù)據(jù)說話。優(yōu)化到什么程度算夠,看業(yè)務(wù)需求。如果10秒已經(jīng)夠用了,沒必要非得優(yōu)化到0.1秒。但如果你知道未來數(shù)據(jù)量會(huì)翻十倍,那提前做準(zhǔn)備就很有必要。

小李后來成了組里的性能優(yōu)化小能手。每次同事吐槽代碼跑得慢,他就會(huì)走過去,看一眼循環(huán),然后說:“你這個(gè)循環(huán),我?guī)湍憧硯椎丁?rdquo;

他辦公室里貼著一張紙條,上面寫著: “Python循環(huán),能不寫就不寫,能少寫就少寫。”

以上就是Python循環(huán)優(yōu)化指南:從10秒到0.1秒的性能調(diào)優(yōu)的詳細(xì)內(nèi)容,更多關(guān)于Python循環(huán)優(yōu)化的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論

额尔古纳市| 阿拉善左旗| 南投市| 西乌| 霞浦县| 元朗区| 宜州市| 连城县| 杂多县| 雅江县| 麻阳| 太仆寺旗| 宁明县| 汝南县| 玉屏| 利津县| 望城县| 玉林市| 深泽县| 武乡县| 新平| 和顺县| 达拉特旗| 康保县| 台中县| 伊宁县| 云林县| 洛扎县| 金堂县| 浮梁县| 汾阳市| 昭苏县| 湖南省| 淄博市| 句容市| 朝阳县| 武功县| 新泰市| 嘉善县| 昂仁县| 微山县|