Python 生成器与 yield 关键字实战:5 个节省内存的高级用法与性能优化技巧
在日常 Python 开发中,我们经常需要处理大量数据。如果把所有数据一次性加载到内存中,很容易导致内存溢出。生成器(Generator)正是解决这个问题的利器。
什么是生成器?
生成器是一种特殊的迭代器,它使用 yield 关键字按需产生值,而不是一次性生成所有结果。与普通函数不同,生成器函数在每次 yield 后会暂停执行,下次调用时从暂停的位置继续。
- 基础用法:读取大文件逐行处理
假设我们要分析一个 10GB 的日志文件,如果用列表存储所有行,内存直接爆掉。使用生成器可以逐行读取,内存占用始终保持在最低水平:
def read_large_file(file_path):
with open(file_path, 'r') as f:
for line in f:
yield line.strip()
for line in read_large_file('access.log'):
if 'ERROR' in line:
print(line)
这种方式的优势在于,无论文件有多大,内存中同时只存在一行数据。
- 生成器表达式:简洁的数据管道
生成器表达式用类似列表推导式的语法,但返回的是生成器对象而非列表,内存占用微乎其微:
numbers = range(10000000)
squared_even = (x * x for x in numbers if x % 2 == 0)
for val in squared_even:
if val > 1000:
break
print(val)
对比使用方括号的列表推导式,圆括号的生成器表达式不会预先创建整个列表,只在实际迭代时才计算每个值。
- yield from:组合多个生成器
Python 3.3 引入了 yield from 语法,可以将多个生成器串联起来,形成数据管道:
def read_config():
yield 'debug=True'
yield 'port=8080'
def read_data():
yield 'row1'
yield 'row2'
def pipeline():
yield from read_config()
yield '---'
yield from read_data()
for item in pipeline():
print(item)
这种模式非常适合构建 ETL 数据管道,每个阶段独立维护自己的逻辑。
- 生成器实现无限序列:斐波那契数列
生成器可以表示无限序列,因为值是按需计算的,不会占用无限内存:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
for _ in range(10):
print(next(fib))
配合 itertools.islice 可以安全地取前 N 项,不用担心无限循环问题。
- 协程与生成器:send 方法实现双向通信
生成器不仅能产出值,还能通过 send 方法接收外部传入的数据,这是 Python 早期协程的基础:
def accumulator():
total = 0
while True:
value = yield total
if value is not None:
total += value
acc = accumulator()
next(acc) # 启动生成器
print(acc.send(10)) # 输出 10
print(acc.send(20)) # 输出 30
print(acc.send(5)) # 输出 35
这种模式可以用于实现流式计算、实时统计等场景。
- 性能对比:生成器 vs 列表
我们用一个简单的实验来量化生成器的内存优势:
import sys
import time
data = list(range(1000000))
列表方式
list_result = [x * 2 for x in data]
print(f'列表占用内存: {sys.getsizeof(list_result)} 字节')
生成器方式
gen_result = (x * 2 for x in data)
print(f'生成器占用内存: {sys.getsizeof(gen_result)} 字节')
典型结果是列表占用约 8MB,而生成器仅占用不到 200 字节,差距超过 40000 倍。
- 实战案例:生成器实现生产者消费者模式
import threading
import queue
import time
def producer(q, n):
for i in range(n):
q.put(i)
time.sleep(0.1)
q.put(None) # 结束信号
def consumer(q):
while True:
item = q.get()
if item is None:
break
print(f'处理: {item}')
q = queue.Queue(maxsize=10)
t1 = threading.Thread(target=producer, args=(q, 20))
t2 = threading.Thread(target=consumer, args=(q,))
t1.start()
t2.start()
t1.join()
t2.join()
虽然这个例子用的是 queue,但生成器配合 yield 同样可以实现类似的生产者消费者模式,而且代码更加简洁优雅。
总结
生成器是 Python 中最被低估的特性之一。它的核心价值在于延迟计算和内存高效,特别适合以下场景:
-
处理大规模数据集,避免内存溢出
-
构建数据管道,实现流式处理
-
表示无限序列,按需生成
-
替代回调,简化异步代码逻辑
下次遇到需要处理大量数据的场景时,先想想能不能用生成器来优化,往往会有意想不到的效果。