写法上的微小区别
[x ** 2 for x in range(10)] # 列表推导式:返回 list
(x ** 2 for x in range(10)) # 生成器表达式:返回生成器
只有方括号 vs 圆括号的区别,但内部机制天差地别。
内存对比
import sys
lst = [x for x in range(10_000_000)]
gen = (x for x in range(10_000_000))
print(sys.getsizeof(lst)) # ~80 MB
print(sys.getsizeof(gen)) # ~200 字节!
生成器几乎不占内存——它用完一个再生成一个。
速度对比
import time
# 求和 10M 个平方
nums = range(10_000_000)
t0 = time.time()
sum([x ** 2 for x in nums])
print(time.time() - t0) # ~1.0s(含建大列表的开销)
t0 = time.time()
sum(x ** 2 for x in nums)
print(time.time() - t0) # ~0.7s
当下游只是迭代一次的时候,生成器更快——少了"建列表"这一步。
怎么选
| 场景 | 用谁 |
|---|---|
| 数据要重复访问 / 切片 / 索引 | 列表 |
| 只迭代一次 | 生成器 |
| 数据量很大或无限 | 生成器(必须) |
| 数据量小 + 后面要 print 看 | 列表(生成器只能看一次) |
| 函数参数 | 生成器:sum(x*2 for x in nums) |
函数参数的"括号省略"
很多函数允许省略生成器表达式的外层括号:
sum(x ** 2 for x in range(10)) # OK
max(len(s) for s in words) # OK
" ".join(s.upper() for s in words) # OK
sorted(x ** 2 for x in nums) # OK
生成器是一次性的
这是它最大的"陷阱":
gen = (x for x in [1, 2, 3])
list(gen) # [1, 2, 3]
list(gen) # [] —— 已经空了!
要重复访问,要么用列表,要么把生成器包进函数每次调用:
def make_gen():
return (x for x in [1, 2, 3])
list(make_gen()) # [1, 2, 3]
list(make_gen()) # [1, 2, 3]
实战:流式读 + 流式处理
def read_large_csv(path):
with open(path, encoding="utf-8") as f:
next(f) # 跳过表头
for line in f:
yield line.strip().split(",")
def filter_by_date(rows, year):
for row in rows:
if row[0].startswith(year):
yield row
def to_floats(rows):
for row in rows:
yield [float(x) for x in row[1:]]
# 三层管道,全程不存中间列表
total = sum(
sum(values)
for values in to_floats(filter_by_date(read_large_csv("big.csv"), "2026"))
)
这种"管道式"处理在数据工作中超级常见。
下一篇讲上下文管理器——with 关键字背后的机制。