3. 生成器与迭代器:列表推导式 vs 生成器表达式、yield关键字详解、懒加载模式实战
这一章我们来聊聊 Python 里一个特别实用的内存优化手段——生成器与迭代器。说实话,我见过太多人一上来就用列表推导式,结果内存直接爆了。我自己也踩过这个坑,后来才真正理解了「懒加载」的威力。
3.1 列表推导式 vs 生成器表达式
先看一个最常见的场景。你有一个大列表,想对它做点变换。很多人会这么写:
# 列表推导式——一次性生成所有数据
squares = [x**2 for x in range(1000000)]
print(len(squares)) # 1000000,内存里全占着
这段代码跑起来,内存占用直接飙升。为什么?因为列表推导式会一次性把所有元素算出来,存到内存里。你想想看,如果数据量是 1000 万呢?
换成生成器表达式:
# 生成器表达式——按需生成,不占内存
squares_gen = (x**2 for x in range(1000000))
print(squares_gen) # <generator object <genexpr> at 0x...>
嗯,这里要注意。生成器表达式用的是圆括号,不是方括号。它不会立即计算,而是返回一个生成器对象。只有当你迭代它的时候,才会一个一个地产生值。
核心区别一句话:列表推导式是「全都要」,生成器表达式是「用的时候再说」。
我在项目中遇到过这样一个情况:处理一个 500 万行的日志文件,同事用列表推导式做过滤,结果内存直接飙到 2GB,程序卡死了。换成生成器表达式后,内存占用降到 50MB 左右。差距就是这么大。
| 对比维度 | 列表推导式 | 生成器表达式 |
|---|---|---|
| 语法 | [expr for item in iterable] |
(expr for item in iterable) |
| 计算时机 | 立即计算全部 | 惰性计算,按需生成 |
| 内存占用 | 高(O(n)) | 低(O(1)) |
| 可重复使用 | 可以多次遍历 | 只能遍历一次 |
| 适用场景 | 数据量小,需要多次访问 | 数据量大,只需遍历一次 |
我的建议:如果你不确定数据量有多大,优先用生成器表达式。等确定数据量很小、需要多次访问时,再转成列表也不迟。
3.2 yield 关键字详解
生成器表达式适合简单的变换。但如果你需要更复杂的逻辑——比如带状态、带条件判断、甚至需要暂停恢复——那就得用 yield 了。
说白了,yield 就是函数的「暂停键」。普通函数遇到 return 就结束了,但 yield 会让函数暂停,把值抛出来,下次调用时从暂停的地方继续。
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('huge_log.txt'):
process(line) # 每次只处理一行,内存友好
我记得有一次要处理一个 10GB 的 CSV 文件。如果用 readlines(),内存直接炸。用 yield 逐行读取,内存占用始终只有几十 KB。这就是懒加载的威力。
再来看一个更复杂的例子——带状态的生成器:
def fibonacci():
"""生成斐波那契数列,无限序列"""
a, b = 0, 1
while True:
yield a
a, b = b, a + b
# 只取前10个
fib = fibonacci()
for _ in range(10):
print(next(fib), end=' ') # 0 1 1 2 3 5 8 13 21 34
注意看,这个生成器永远不会结束(while True)。但因为它用 yield 暂停了,所以不会占满内存。你取多少,它就生成多少。
曾经踩过的坑:我曾经在生成器里用了 return 而不是 yield,结果函数只返回第一个值就结束了。记住:生成器函数里用 yield,不要用 return 返回值(return 会触发 StopIteration)。
3.3 懒加载模式实战
懒加载(Lazy Loading)说白了就是「用到才加载」。这个模式在资源压缩里特别重要。我们来看几个实战场景。
场景一:分页查询
假设你要从数据库查 100 万条记录。一次性查出来?内存扛不住。用懒加载:
def paginated_query(cursor, table, page_size=1000):
"""懒加载分页查询,每次只取一页"""
offset = 0
while True:
sql = f"SELECT * FROM {table} LIMIT {page_size} OFFSET {offset}"
cursor.execute(sql)
rows = cursor.fetchall()
if not rows:
break
for row in rows:
yield row
offset += page_size
# 使用
for row in paginated_query(cursor, 'users'):
process_user(row) # 每次只处理1000条
这个模式我用了很多次。数据库查询最怕的就是一次性加载太多数据。用生成器做分页,内存占用可控,代码还简洁。
场景二:树形结构遍历
处理文件系统、组织架构等树形数据时,懒加载特别有用:
def walk_files(root_dir):
"""递归遍历目录,懒加载每个文件"""
for entry in os.scandir(root_dir):
if entry.is_dir():
yield from walk_files(entry.path) # yield from 委托给子生成器
else:
yield entry.path
# 使用
for file_path in walk_files('/var/log'):
if file_path.endswith('.log'):
analyze(file_path) # 一次只处理一个文件
这里用了 yield from,它是 Python 3.3 引入的语法糖。说白了就是把子生成器的 yield 委托给外层,省得你手动写循环。
场景三:数据管道
懒加载最强大的用法是串联成管道。每个生成器只做一件事,然后串起来:
def read_lines(file_path):
with open(file_path) as f:
for line in f:
yield line
def filter_errors(lines):
for line in lines:
if 'ERROR' in line:
yield line
def parse_timestamp(lines):
for line in lines:
parts = line.split(' ')
yield {'timestamp': parts[0], 'message': ' '.join(parts[1:])}
# 串联管道——数据流式处理,内存占用极低
pipeline = parse_timestamp(
filter_errors(
read_lines('app.log')
)
)
for record in pipeline:
save_to_db(record) # 每次只处理一条记录
你看,每个函数只做一件事,通过生成器串联起来。数据像流水一样流过每个环节,内存里永远只有一条记录。这就是懒加载模式的精髓。
懒加载的核心原则:
- 绝不一次性加载全部数据
- 每次只处理一个元素
- 用生成器串联成数据管道
- 内存占用 = 单个元素大小,与数据总量无关
下面这张图展示了生成器与迭代器的核心关系:
实战建议:我一般这样选:
- 简单变换 → 生成器表达式
(x for x in data) - 复杂逻辑 → 生成器函数
def gen(): yield x - 需要状态 → 生成器函数 + 局部变量
- 需要委托 →
yield from sub_generator()
最后说一句:生成器和迭代器不是银弹。如果你需要多次遍历数据,或者数据量很小,列表推导式反而更合适。但如果你在做资源压缩、内存优化,生成器绝对是你工具箱里最趁手的工具之一。