第一部分:语法与核心区别(仅差一个括号)
语法上,生成器表达式把列表推导式的 [] 换成 () ,但背后的运行机制天差地别。
python
# 列表推导式:立即计算,返回 list
list_comp = [x**2 for x in range(5)]
print(list_comp) # [0, 1, 4, 9, 16]
print(type(list_comp)) # <class 'list'>
# 生成器表达式:惰性计算,返回 generator 对象
gen_exp = (x**2 for x in range(5))
print(gen_exp) # <generator object <genexpr> at 0x...>
print(type(gen_exp)) # <class 'generator'>
内存占用对比(实战震撼)
假设要处理 100 万个数字,列表推导式会瞬间吃掉大量内存,而生成器表达式几乎不占内存:
python
import sys
list_comp = [i for i in range(1000000)]
gen_exp = (i for i in range(1000000))
print(sys.getsizeof(list_comp)) # 约 8MB(实际取决于系统)
print(sys.getsizeof(gen_exp)) # 仅 112 字节(固定大小!)
第二部分:生成器表达式的独有特性——“一次性”
生成器是一个迭代器,它像“单程票”,遍历完就没了,不能回头。
python
gen = (x for x in ['A', 'B', 'C'])
# 第一次遍历
print(list(gen)) # ['A', 'B', 'C']
# 第二次遍历(已经耗尽)
print(list(gen)) # [] —— 空的!没有报错,但数据已丢失
避坑:如果你想复用数据,就别用生成器表达式,老老实实用列表。
第三部分:与面向对象(OOP)和类型提示的联动
在类的设计中,返回生成器表达式是处理“大数据集合”的标准模式。
类型提示:生成器的类型是 Generator[YieldType, SendType, ReturnType],但通常你用 Iterator[YieldType] 标注返回值就足够了。
python
from typing import Iterator
class LargeLogReader:
"""大型日志文件读取器(OOP 封装)"""
def __init__(self, file_path: str) -> None:
self.file_path = file_path
# 返回生成器,内存友好
def filter_errors(self, keyword: str = "ERROR") -> Iterator[str]:
"""逐行读取,包含特定关键词的行(生成器表达式)"""
with open(self.file_path, 'r', encoding='utf-8') as f:
# 生成器表达式:不一次读入全部,逐行实时过滤
return (line.strip() for line in f if keyword in line)
# 另一种写法:yield 函数(功能相同,写法不同)
def filter_errors_yield(self, keyword: str = "ERROR") -> Iterator[str]:
with open(self.file_path, 'r', encoding='utf-8') as f:
for line in f:
if keyword in line:
yield line.strip()
调用时,如果外部只要前 10 行,生成器只读取前 10 行日志,绝不会把整个几十 GB 的日志文件加载到内存。
第四部分:与异步编程(Async)的特殊关系(高阶)
非常重要:在异步函数 async def 中,不能直接使用 await 在生成器表达式内部(因为 await 必须出现在函数内)。
但是,你可以用生成器表达式批量构建异步任务,或者在异步循环中消费生成器。
实战场景:异步批量下载文件,但限制并发数量,且结果用生成器流式吐出。
python
import asyncio
from typing import Iterator, Awaitable, Any
async def fetch_data(url: str) -> dict[str, Any]:
await asyncio.sleep(0.5) # 模拟网络 IO
return {"url": url, "data": "fake_data"}
async def process_batch(urls: list[str]) -> None:
# 生成器表达式 生成协程对象(注意:这里没有执行,只是懒构建)
coroutines = (fetch_data(url) for url in urls)
# 注意:生成器内部的协程并未启动。我们可以逐次消费它,限制并发
results = []
for coro in coroutines:
result = await coro # 每次 await 一个,相当于逐个执行(非并发)
results.append(result)
# 如果想并发,必须用 asyncio.gather,但那样就不需要生成器了,直接用列表
结论:在异步中,生成器表达式适合逐个处理流式数据(如分页 API 响应),而列表推导式适合
asyncio.gather(*[fetch(i) for i in items])进行批量并发。
第五部分:何时必须用生成器表达式?(黄金法则)
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 数据集极大(百万级以上) | 生成器表达式 | 防止内存溢出(OOM) |
只需遍历一次(如 sum、any、all) | 生成器表达式 | 计算完即丢弃,省内存且代码简洁 |
| 需要切片、反复索引、排序、多次遍历 | 列表推导式 | 生成器不支持索引,无法复用 |
| 求总和、最大值等聚合操作 | sum(x**2 for x in range(1e7)) | 妙用:注意这里不用双层括号,sum 自认生成器 |
经典一行代码(极简高性能) :
python
# 求 1 亿以内偶数的平方和(内存占用几乎为 0)
total = sum(x**2 for x in range(100_000_000) if x % 2 == 0)
print(total)
避坑终极总结
-
别把它当列表用:生成器没有
len(),没有[0]索引,只能next()或for遍历。 -
小心
any()和all()的短路效应:python
gen = (x for x in [1, 2, 0, 5]) print(any(gen)) # True(遇到 1 就停了,后面的 2,0,5 没消费) print(list(gen)) # [2, 0, 5] —— 1 已经没了! -
性能权衡:生成器生成元素有计算开销。如果数据量小,列表推导式比生成器表达式更快(因为生成器有状态切换损耗)。数据量小(<1000)用列表,数据量大用生成器。