列表推导式"更快"——但快在哪里,什么时候快,什么时候根本没差?
网上随便搜,都能找到"列表推导式比 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 模块都支持。



加载中...