列表推導式"更快"——但快在哪裏,什麼時候快,什麼時候根本沒差?
網上隨便搜,都能找到"列表推導式比 for 循環快"這個說法。大部分文章給你跑個 timeit,展示列表推導式快了 2 倍,然後說用列表推導式吧,完事。
但實際寫代碼的時候經常會發現:換成列表推導式,壓根看不出快了多少。這不是錯覺,是有原因的。
先說結論,再講原因
- 簡單操作(乘以2、加1、類型轉換這類):列表推導式比 for 循環快 20%-50%,有時接近 2 倍
- 複雜操作(函數調用、正則匹配、IO、數據庫查詢這類):基本沒有差別,快不了幾個毫秒
- 數據量很大、內存吃緊:列表推導式反而是問題,生成器表達式纔是正確選擇
爲什麼簡單操作時列表推導式更快?
用 dis 模塊看一下字節碼就清楚了:
import dis
# for 循環版本
def for_loop():
result = []
for i in range(10):
result.append(i * 2)
return result
# 列表推導式版本
def list_comp():
return [i * 2 for i in range(10)]
dis.dis(for_loop)
dis.dis(list_comp)
for 循環的字節碼裏,每次迭代都要做:
LOAD_FAST:找到result變量LOAD_ATTR:查找.append屬性——這步最貴CALL_FUNCTION:調用 append
列表推導式用的是專門的字節碼指令 LIST_APPEND,繞過了屬性查找,直接在 C 層面操作列表。循環10萬次,省了10萬次屬性查找,差距就出來了。
用 timeit 驗證一下:
import timeit
# 簡單操作:每個數乘以2
setup = "data = list(range(1_000_000))"
t_for = timeit.timeit(
"[result.append(x * 2) for _ in [None] if False] or [x * 2 for x in data]",
setup=setup, number=10
)
# 更直觀的對比
def for_loop(data):
result = []
for x in data:
result.append(x * 2)
return result
def list_comp(data):
return [x * 2 for x in data]
data = list(range(1_000_000))
t1 = timeit.timeit(lambda: for_loop(data), number=50)
t2 = timeit.timeit(lambda: list_comp(data), number=50)
print(f"for loop: {t1:.3f}s")
print(f"list comp: {t2:.3f}s")
print(f"快了: {t1/t2:.2f}x")
# 典型輸出:
# for loop: 2.341s
# list comp: 1.487s
# 快了: 1.57x
爲什麼複雜操作時沒有差別?
道理很簡單:當操作本身耗時遠超循環開銷,那點屬性查找的時間就是九牛一毛。
import math
import timeit
def heavy_for(data):
result = []
for x in data:
result.append(math.sqrt(x) ** 2.5 + math.log(x + 1))
return result
def heavy_comp(data):
return [math.sqrt(x) ** 2.5 + math.log(x + 1) for x in data]
data = list(range(1, 100_001))
t1 = timeit.timeit(lambda: heavy_for(data), number=20)
t2 = timeit.timeit(lambda: heavy_comp(data), number=20)
print(f"for loop: {t1:.3f}s")
print(f"list comp: {t2:.3f}s")
# 差距往往在 5% 以內,基本可以忽略
如果循環體裏有網絡請求、數據庫查詢、文件讀寫,那 for 循環和列表推導式的性能差距就更加可以忽略——網絡延遲動輒幾十毫秒,append 的開銷根本不值一提。
內存:列表推導式的隱患
列表推導式是"餓漢",執行的瞬間就把所有結果放進內存。
# 這行代碼會立刻佔用幾百 MB 內存
big_list = [x * 2 for x in range(10_000_000)]
# 生成器表達式是"懶漢",幾乎不佔內存
big_gen = (x * 2 for x in range(10_000_000))
import sys
print(sys.getsizeof(big_list)) # ~85 MB
print(sys.getsizeof(big_gen)) # 200 Bytes 左右
如果你只是要遍歷結果一次(比如求和、找最大值、傳給另一個函數),根本不需要列表推導式,直接用生成器表達式:
# 不需要建完整列表
total = sum(x * 2 for x in range(10_000_000)) # 內存友好
maximum = max(x ** 2 for x in range(10_000_000)) # 內存友好
# 而不是
total = sum([x * 2 for x in range(10_000_000)]) # 先建完整列表再求和,浪費
什麼時候不該用列表推導式
邏輯複雜的時候。 列表推導式超過兩層條件或者嵌套兩層以上,可讀性會急劇下降,這時候老老實實寫 for 循環反而更好:
# 這已經很難看懂了
result = [x for sublist in matrix for x in sublist if x > 0 if x % 2 == 0]
# 換成 for 循環,起碼能加註釋
result = []
for sublist in matrix:
for x in sublist:
if x > 0 and x % 2 == 0:
result.append(x)
需要中途 break 的時候。 列表推導式沒法 break,找到第一個滿足條件的元素就停下來,只能用 for 循環或者 next():
# 找第一個大於100的數,列表推導式做不到高效
data = range(1_000_000)
# 錯誤的做法:把100萬個元素全遍歷了
first = [x for x in data if x > 100][0]
# 正確的做法
first = next(x for x in data if x > 100)
# 或者
for x in data:
if x > 100:
first = x
break
需要處理異常的時候。 列表推導式裏沒辦法 try-except:
# 有些字符串可能轉換失敗,只能用 for 循環
def safe_int(s):
try:
return int(s)
except ValueError:
return None
data = ["1", "abc", "3", "xyz"]
result = [safe_int(x) for x in data] # 這樣可以,把異常處理封進函數
# 但不能在推導式內部直接寫 try-except
一張表總結
| 場景 | 推薦寫法 |
|---|---|
| 簡單變換,結果要放進列表 | 列表推導式 |
| 只需遍歷一次結果(求和/最大值等) | 生成器表達式 |
| 數據量超大,內存有限 | 生成器表達式 |
| 操作複雜(函數調用、IO) | 隨意,可讀性優先 |
| 邏輯複雜,需要註釋 | for 循環 |
| 需要 break 或 continue | for 循環 |
| 需要 try-except | for 循環,或把異常處理封進函數 |
想直接跑這些代碼驗證結果,粘到 Python 在線運行工具 裏就行,Python 3.12 環境,timeit 和 dis 模塊都支持。



加載中...