有段时间我处理一个日志文件,文件大小 3GB,用 readlines() 一口气读进来,直接把机器干趴了。
内存占用飙到 2GB,风扇呼呼转,程序跑到一半报 MemoryError,什么都没捞到。
后来换了生成器,同样的文件,内存稳稳维持在 20MB 以内,跑完整个文件没有任何问题。
那一刻我才真正理解生成器是干什么用的——不是什么高级语法糖,是救命的东西。
列表和生成器的本质区别
先说最直白的对比。
列表是把所有数据一次性塞进内存:
# 一口气生成 100 万个数,全部住进内存
data = [x * x for x in range(1_000_000)]
print(data[:5]) # [0, 1, 4, 9, 16]
生成器是"用的时候再算,用完就扔":
# 生成器表达式,一个数都没算,只是定义了规则
gen = (x * x for x in range(1_000_000))
print(next(gen)) # 0
print(next(gen)) # 1
print(next(gen)) # 4
next() 每调用一次,才计算一个值,计算完就交给你,自己不留着。
用 sys.getsizeof 看看差距:
import sys
lst = [x * x for x in range(1_000_000)]
gen = (x * x for x in range(1_000_000))
print(f"列表大小: {sys.getsizeof(lst) / 1024 / 1024:.2f} MB")
print(f"生成器大小: {sys.getsizeof(gen)} bytes")
输出:
列表大小: 7.63 MB
生成器大小: 104 bytes
生成器对象本体只有 104 个字节。不管你说要生成多少个数,它都只占这么点空间。
yield 关键字:函数变生成器的魔法
用 yield 写一个生成器函数,原理比表达式更直观:
def square_gen(n):
for i in range(n):
yield i * i # 暂停在这里,把值交出去,等下次 next() 再继续
gen = square_gen(5)
for val in gen:
print(val)
输出:
0
1
4
9
16
yield 和 return 的区别在于:return 是结束,yield 是暂停。函数执行到 yield,把值抛出去,然后冻住,等你下次调用 next() 时从这里接着跑。
这就是为什么生成器适合处理"无限序列"——比如实时传感器数据、日志流,你不可能把无限数据装进列表,但生成器可以:
def counter(start=0):
n = start
while True: # 无限循环,但没关系
yield n
n += 1
c = counter(10)
print(next(c)) # 10
print(next(c)) # 11
print(next(c)) # 12
# 只要你调用,就一直有值
回到那个 3GB 日志文件
用生成器读大文件,是我日常用得最多的场景:
def read_large_file(filepath):
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
yield line.strip()
# 使用
for line in read_large_file('huge_log.txt'):
if 'ERROR' in line:
print(line)
每次只把一行内容装进内存,处理完就释放,文件再大也不怕。
如果要加工处理,可以套多个生成器,像流水线一样:
def read_lines(filepath):
with open(filepath, 'r') as f:
for line in f:
yield line.strip()
def filter_errors(lines):
for line in lines:
if 'ERROR' in line:
yield line
def parse_time(lines):
for line in lines:
parts = line.split(' ')
yield {'time': parts[0], 'msg': ' '.join(parts[1:])}
# 组装流水线,三个生成器串联,数据逐条流过
pipeline = parse_time(filter_errors(read_lines('app.log')))
for record in pipeline:
print(record)
这个模式叫"生成器管道",整个过程内存里永远只有一条数据,效率高得离谱。
什么时候该用,什么时候不该用
用生成器的场景:
- 数据量大,一次性读不起
-
- 数据是流式的(网络请求、实时日志)
-
- 只遍历一次,不需要随机访问
-
不适合生成器的场景:
- 需要多次遍历同一批数据(生成器用完就空了)
-
- 需要
len()、索引、切片操作 -
- 数据量本来就小,用列表更直接
-
最后一个容易踩的坑:生成器只能遍历一次。
gen = (x for x in range(5))
print(list(gen)) # [0, 1, 2, 3, 4]
print(list(gen)) # [] -- 已经空了!
如果要复用,要么转成列表,要么重新创建生成器。
内存问题往往不是写了什么,而是有没有意识到"东西能不能不一次全放进来"。生成器就是这种意识的工具化实现。
如果你平时处理的数据还不大,也可以现在就养成这个习惯——懒计算、用时取,代码优雅,机器也轻松。
下次碰到大数据集,先别急着 pd.read_csv 一把梭,想想能不能先用生成器过滤一下。
最后,别忘了关注「有为大青年」,我们下期见~
有没有用生成器解决过什么实际问题?欢迎留言聊聊,说不定能帮到别人~