Python 生成器入门:用 yield 写出省内存的数据处理流水线
处理大文件时,最常见的错误是把它一次性读进内存。一个几百兆的日志文件,用 readlines() 展开成列表,内存占用立刻翻倍,程序还没开始干活就先吃掉了大半资源。生成器正是为了解决这个问题而生的:它不急于算出全部结果,而是需要一个、产出一个,用多少、算多少。
一、yield:让函数学会“暂停”
普通函数执行到 return 就会结束并销毁局部状态,而生成器函数使用 yield。每次执行到 yield,函数会把值交出去并原地暂停,保留全部局部变量;下一次调用 next(),它从暂停处继续执行。
def count_up(n):
i = 1
while i <= n:
yield i
i += 1
for num in count_up(3):
print(num) # 依次输出 1、2、3
这里没有一次性生成 1 到 n 的列表,内存占用始终只有几个变量。for 循环能直接遍历生成器,就像遍历列表一样自然。
二、生成器表达式:一行写出惰性序列
列表推导式用方括号,生成器表达式把方括号换成圆括号,语义立刻从“立即算完”变成“按需计算”:
squares = (x * x for x in range(10**6))
print(next(squares)) # 只算出第一个值 0
一百万个平方数不会同时存在于内存里,取一个算一个。需要全部结果时再用 list() 展开,但那会失去惰性的意义,一般只在数据量确实很小时才这样做。
三、串联成流水线:日志处理实战
生成器的真正威力在于串联。每个环节只处理一行数据,整条流水线一次只在内存中保留少量数据:
def read_lines(path):
with open(path, encoding='utf-8') as f:
for line in f:
yield line.strip()
def only_errors(lines):
for line in lines:
if 'ERROR' in line:
yield line
def take_fields(lines):
for line in lines:
yield line.split(' ', 2)[-1]
for msg in take_fields(only_errors(read_lines('app.log'))):
print(msg)
读文件、过滤、提取字段三步各自独立,组合时却像接水管一样自然。换掉其中任何一环,流水线其余部分都不用改;数据量从几兆涨到几个 G,内存占用几乎不变。
四、两个容易踩的坑
第一,生成器只能遍历一次。数据被消费后就消失了,需要重复使用时要重新创建,或者先用 list() 物化一份。第二,yield 和带值的 return 不能混用:在生成器函数里写带值的 return 会直接触发 StopIteration,初学者常把“返回值”当成“产出值”,结果丢了数据。
小结
生成器不是炫技的语法糖,而是一种组织数据处理的方式:把“一次性算完”改成“用多少算多少”。下次遇到大文件、长序列、无限数据流时,先想想能不能用 yield 搭一条流水线——内存会立刻轻松下来,代码也会比层层嵌套的循环清爽得多。
- 点赞
- 收藏
- 关注作者
评论(0)