Python 生成器表达式与列表推导式深度解析:一对括号背后的内存、性能与工程取舍
在 Python 编程中,我们经常需要对一组数据进行转换、筛选和聚合。
例如:
- 把商品价格统一换算成含税价格;
- 从日志中提取错误记录;
- 读取大型文件并清洗数据;
- 对接口返回的用户列表进行过滤;
- 统计满足条件的数据总量。
面对这些任务,列表推导式和生成器表达式都是非常常见的工具。
它们的语法看起来几乎一样:
# 列表推导式
squares = [number ** 2 for number in range(10)]
# 生成器表达式
squares = (number ** 2 for number in range(10))
二者似乎只相差一对括号:
- 列表推导式使用方括号
[]; - 生成器表达式使用圆括号
()。
但这对括号背后,隐藏的是两种完全不同的执行模型。
列表推导式会立即完成全部计算,并把结果保存在列表中;生成器表达式则采用惰性求值,只有当程序真正请求下一个元素时,它才执行相应计算。
这一区别会直接影响:
- 代码的内存占用;
- 首个结果的返回速度;
- 数据能否重复遍历;
- 异常出现的时间;
- 是否适合处理无限数据流;
- 程序的可读性和可维护性。
如果只记住一句话,可以记住:
列表推导式保存结果,生成器表达式保存计算方法。
本文将围绕这个核心区别,深入分析二者的语义、性能、适用场景与常见陷阱,帮助你在真实的 Python 项目中做出正确选择。
一、从最简单的例子开始
假设我们要计算 0 到 4 的平方。
使用列表推导式
squares = [number ** 2 for number in range(5)]
print(squares)
print(type(squares))
输出:
[0, 1, 4, 9, 16]
<class 'list'>
当这一行代码执行结束后,所有平方值都已经计算完成,并被保存在列表对象中。
因此,我们可以通过索引访问:
print(squares[0])
print(squares[-1])
也可以反复遍历:
for value in squares:
print(value)
for value in squares:
print(value)
两次循环都会输出完整结果。
使用生成器表达式
squares = (number ** 2 for number in range(5))
print(squares)
print(type(squares))
输出形式类似:
<generator object <genexpr> at 0x...>
<class 'generator'>
此时还没有生成包含五个元素的列表。变量 squares 保存的是一个生成器对象。
可以通过 next() 逐个取值:
print(next(squares))
print(next(squares))
print(next(squares))
输出:
0
1
4
每调用一次 next(),生成器才向前执行一步。
继续遍历:
for value in squares:
print(value)
输出:
9
16
前面的三个元素已经被消费,因此循环只会得到剩余两个元素。
这揭示了生成器的第一个重要特征:
生成器是有状态、向前移动、通常只能完整消费一次的迭代器。
二、核心区别:立即求值与惰性求值
列表推导式采用立即求值。
result = [transform(item) for item in items]
执行这一行时,Python 会:
- 遍历
items; - 对每个元素调用
transform; - 将结果依次加入列表;
- 返回完整列表。
其流程可以表示为:
输入数据
↓
依次计算全部元素
↓
保存所有结果
↓
返回完整列表
生成器表达式采用惰性求值:
result = (transform(item) for item in items)
创建生成器时,Python 主要保存:
- 当前执行位置;
- 原始可迭代对象;
- 转换表达式;
- 局部状态。
只有外部请求数据时,它才执行计算:
创建计算规则
↓
外部请求一个元素
↓
计算并返回一个元素
↓
暂停并保存状态
↓
等待下一次请求
例如:
def transform(number):
print(f"正在处理:{number}")
return number * 2
列表推导式:
result = [transform(number) for number in range(3)]
print("列表已经创建")
输出:
正在处理:0
正在处理:1
正在处理:2
列表已经创建
生成器表达式:
result = (transform(number) for number in range(3))
print("生成器已经创建")
print(next(result))
输出:
生成器已经创建
正在处理:0
0
生成器创建时并没有调用 transform。只有执行 next(result) 时,第一个元素才被处理。
三、内存占用:为什么生成器适合大数据?
假设需要计算一百万个整数的平方。
列表推导式会创建完整列表:
squares = [
number ** 2
for number in range(1_000_000)
]
这个列表需要同时保存一百万个结果对象的引用,以及对应整数对象所占用的空间。
生成器表达式则不会保存全部结果:
squares = (
number ** 2
for number in range(1_000_000)
)
它只保留生成下一个值所需要的状态,内存占用通常远低于列表。
可以使用 sys.getsizeof() 观察对象本身的大小:
import sys
list_result = [
number ** 2
for number in range(1_000_000)
]
generator_result = (
number ** 2
for number in range(1_000_000)
)
print(sys.getsizeof(list_result))
print(sys.getsizeof(generator_result))
通常会看到:
- 列表对象占用数 MB;
- 生成器对象本身只占用几百字节。
不过需要注意,sys.getsizeof() 只统计对象本身,不会递归统计列表中所有元素的完整内存。
更严谨的方式是使用 tracemalloc:
import tracemalloc
def consume(iterable):
total = 0
for value in iterable:
total += value
return total
def measure_peak_memory(factory):
tracemalloc.start()
result = consume(factory())
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
return result, peak
list_total, list_peak = measure_peak_memory(
lambda: [
number ** 2
for number in range(1_000_000)
]
)
generator_total, generator_peak = measure_peak_memory(
lambda: (
number ** 2
for number in range(1_000_000)
)
)
print(f"列表峰值内存:{list_peak / 1024 / 1024:.2f} MB")
print(f"生成器峰值内存:{generator_peak / 1024 / 1024:.2f} MB")
由于生成器一次只产生一个结果,消费完当前值后,该值通常就可以被回收,因此峰值内存明显更低。
这使生成器特别适合以下场景:
- 读取大型日志文件;
- 处理海量 CSV 数据;
- 流式解析网络响应;
- 处理数据库游标;
- 消费消息队列;
- 构建多阶段数据处理管道。
四、性能区别:生成器不一定“更快”
很多初学者容易产生一个误解:
生成器更节省内存,所以一定比列表推导式更快。
实际上,内存效率和执行速度是两个不同问题。
列表推导式一次性完成全部计算,创建列表后,后续访问速度很快。生成器每次取值都需要恢复执行状态、运行表达式、暂停并保存状态,存在一定的迭代调度开销。
可以使用 timeit 进行测试。
测试完整消费结果
import timeit
list_time = timeit.timeit(
"sum([number * 2 for number in range(1000)])",
number=100_000,
)
generator_time = timeit.timeit(
"sum(number * 2 for number in range(1000))",
number=100_000,
)
print(f"列表推导式:{list_time:.4f} 秒")
print(f"生成器表达式:{generator_time:.4f} 秒")
测试结果取决于:
- Python 版本;
- 解释器实现;
- 操作系统;
- CPU;
- 表达式复杂度;
- 数据规模。
在某些情况下,列表推导式可能更快,因为它针对列表构建进行了优化;生成器的优势主要是节省内存,而不是保证运行时间最短。
因此,更准确的说法是:
生成器通常具有更低的内存峰值和更快的首个结果响应,但完整遍历时未必比列表推导式更快。
五、首个结果速度:生成器可以更早开始工作
虽然生成器完整执行未必更快,但它通常可以更早产生第一个结果。
假设每个数据的计算都需要一秒钟:
import time
def slow_transform(number):
time.sleep(1)
return number * 10
使用列表推导式:
result = [
slow_transform(number)
for number in range(5)
]
print(result[0])
程序需要等待约五秒,才能得到第一个可以访问的结果。因为列表必须先全部构造完成。
使用生成器表达式:
result = (
slow_transform(number)
for number in range(5)
)
print(next(result))
大约一秒后,第一个结果就可以返回。
这在以下场景中特别重要:
- 流式接口;
- 实时数据处理;
- 大文件解析;
- 边读取边写入;
- 首屏数据快速展示;
- 长时间运行的数据管道。
生成器让程序能够“边生产、边消费”,不必等待全部数据准备完成。
六、可重复使用性:列表可以重放,生成器会耗尽
列表中的数据已经保存,因此可以多次遍历:
numbers = [
number
for number in range(5)
]
print(sum(numbers))
print(sum(numbers))
输出:
10
10
生成器则会被消费:
numbers = (
number
for number in range(5)
)
print(sum(numbers))
print(sum(numbers))
输出:
10
0
第一次 sum() 已经把生成器中的元素全部取完。第二次调用时,生成器已经耗尽,没有任何元素可供计算。
这类问题在真实项目中非常隐蔽。
例如:
active_users = (
user
for user in users
if user["active"]
)
active_count = sum(1 for _ in active_users)
send_notifications(active_users)
这里统计用户数量后,active_users 已经被消费,后续通知函数将收不到任何用户。
可以改为列表:
active_users = [
user
for user in users
if user["active"]
]
active_count = len(active_users)
send_notifications(active_users)
或者重新创建生成器:
def iter_active_users(users):
return (
user
for user in users
if user["active"]
)
active_count = sum(
1
for _ in iter_active_users(users)
)
send_notifications(
iter_active_users(users)
)
选择时要问自己:
结果是否需要被重复读取?
如果答案是肯定的,列表通常更合适。
七、随机访问与常用操作的区别
列表支持丰富的序列操作:
numbers = [
number ** 2
for number in range(10)
]
print(numbers[3])
print(numbers[-1])
print(numbers[2:6])
print(len(numbers))
生成器不支持索引和切片:
numbers = (
number ** 2
for number in range(10)
)
print(numbers[3])
会抛出异常:
TypeError: 'generator' object is not subscriptable
生成器也无法直接使用 len():
len(numbers)
同样会抛出 TypeError。
如果确实需要从生成器中获取部分数据,可以使用 itertools.islice:
from itertools import islice
numbers = (
number ** 2
for number in range(100)
)
first_five = list(islice(numbers, 5))
print(first_five)
输出:
[0, 1, 4, 9, 16]
但要注意,被 islice 取出的元素也会从原生成器中消失。
八、异常出现的时间不同
立即求值和惰性求值还会影响异常出现的时机。
定义一个可能失败的函数:
def divide(number):
return 100 / number
列表推导式:
result = [
divide(number)
for number in [10, 5, 0, 2]
]
程序创建列表时就会遇到除零错误:
ZeroDivisionError: division by zero
生成器表达式:
result = (
divide(number)
for number in [10, 5, 0, 2]
)
print("生成器创建成功")
这一阶段通常不会报错,因为计算尚未真正开始。
当消费到数字 0 时,异常才会出现:
for value in result:
print(value)
输出过程类似:
10.0
20.0
ZeroDivisionError: division by zero
这意味着生成器中的错误可能距离定义代码很远。
例如,生成器在函数 A 中创建,却在函数 D 中消费,那么异常可能最终出现在函数 D 的调用过程中。
因此,使用惰性数据管道时,需要特别关注:
- 异常在哪里处理;
- 日志是否包含足够上下文;
- 输入资源是否仍然有效;
- 消费行为是否发生在预期位置。
九、资源生命周期陷阱:文件关闭后再消费
下面是一段看似合理、实际上存在问题的代码:
def read_clean_lines(filename):
with open(filename, "r", encoding="utf-8") as file:
return (
line.strip()
for line in file
if line.strip()
)
调用:
lines = read_clean_lines("data.txt")
for line in lines:
print(line)
函数返回时,with 代码块已经结束,文件被关闭。生成器稍后尝试读取文件,就可能触发:
ValueError: I/O operation on closed file
列表推导式则不会有这个问题:
def read_clean_lines(filename):
with open(filename, "r", encoding="utf-8") as file:
return [
line.strip()
for line in file
if line.strip()
]
因为所有数据都在文件关闭前读取完成。
如果既要流式处理,又要安全管理文件,应把生成器逻辑写成生成器函数:
def iter_clean_lines(filename):
with open(filename, "r", encoding="utf-8") as file:
for line in file:
cleaned = line.strip()
if cleaned:
yield cleaned
调用:
for line in iter_clean_lines("data.txt"):
print(line)
文件会在迭代过程中保持打开,并在迭代结束或生成器关闭时释放。
十、生成器最适合与聚合函数配合
很多 Python 内置函数只需要逐个读取元素,并不要求先创建列表。
例如:
summinmaxanyallnext
这时生成器表达式通常是自然选择。
求和
不推荐无意义地创建中间列表:
total = sum([
order["amount"]
for order in orders
if order["status"] == "paid"
])
更推荐:
total = sum(
order["amount"]
for order in orders
if order["status"] == "paid"
)
这里 sum() 会逐个消费生成器,不需要保存所有金额。
判断是否存在符合条件的元素
has_admin = any(
user["role"] == "admin"
for user in users
)
any() 遇到第一个真值就会立即停止。
如果第一个用户就是管理员,后面的用户根本不需要检查。
同理:
all_valid = all(
validate(item)
for item in items
)
all() 遇到第一个假值就会立即停止。
这种短路求值与生成器结合,可以减少不必要的计算。
查找第一个符合条件的元素
first_admin = next(
(
user
for user in users
if user["role"] == "admin"
),
None,
)
这段代码找到第一个管理员后就停止,不需要扫描并保存所有管理员。
也可以省略生成器表达式外层的额外括号:
first_admin = next(
(
user
for user in users
if user["role"] == "admin"
),
None,
)
当生成器表达式作为函数的唯一位置参数时,部分场景可进一步简写。例如:
total = sum(
number ** 2
for number in range(100)
)
十一、无限序列:列表无法做到,生成器可以
列表必须最终把所有结果保存下来,因此无法表示真正的无限序列。
生成器可以:
def count_from(start=0):
current = start
while True:
yield current
current += 1
使用:
numbers = count_from(10)
print(next(numbers))
print(next(numbers))
print(next(numbers))
输出:
10
11
12
也可以使用 itertools.count:
from itertools import count, islice
numbers = (
number ** 2
for number in count(1)
)
first_ten = list(islice(numbers, 10))
print(first_ten)
生成器表达式本身并没有终点,但 islice 只取前十个结果。
这类模式广泛应用于:
- 序列号生成;
- 事件流;
- 传感器数据;
- 消息消费;
- 分页抓取;
- 持续运行的服务。
十二、真实案例:流式处理大型日志
假设有一个数 GB 的日志文件,我们需要:
- 找出包含
ERROR的记录; - 去除首尾空白;
- 转换为结构化字典;
- 统计错误类型。
如果使用列表推导式逐阶段处理:
with open(
"application.log",
"r",
encoding="utf-8",
) as file:
lines = [line for line in file]
error_lines = [
line.strip()
for line in lines
if "ERROR" in line
]
records = [
parse_error_line(line)
for line in error_lines
]
这里可能同时存在多个大型列表:
- 原始行列表;
- 错误行列表;
- 结构化记录列表。
内存压力会非常大。
使用生成器构建流式管道:
def iter_log_lines(filename):
with open(
filename,
"r",
encoding="utf-8",
) as file:
for line in file:
yield line
lines = iter_log_lines("application.log")
error_lines = (
line.strip()
for line in lines
if "ERROR" in line
)
records = (
parse_error_line(line)
for line in error_lines
)
for record in records:
save_record(record)
数据流如下:
日志文件
↓ 每次读取一行
筛选 ERROR
↓
清洗字符串
↓
解析结构
↓
写入存储
整个过程中,每个阶段只处理当前元素,不需要把完整文件加载到内存。
这就是生成器在 Python 实战中的核心价值:
它不仅是节省内存的语法技巧,更是一种数据流架构。
十三、什么时候应该使用列表推导式?
以下场景通常适合列表推导式。
1. 数据量较小
normalized_names = [
name.strip().title()
for name in names
]
几十、几百或几千个元素通常不值得为了少量内存使用更复杂的惰性结构。
2. 结果需要反复使用
active_users = [
user
for user in users
if user["active"]
]
后续可能需要:
print(len(active_users))
send_emails(active_users)
export_users(active_users)
列表更加方便、安全。
3. 需要索引、切片或倒序访问
recent_orders = [
normalize_order(order)
for order in orders
]
latest = recent_orders[-1]
first_page = recent_orders[:20]
4. 希望错误尽早暴露
列表会在创建时完成计算,数据问题可以在当前代码位置立即发现。
5. 结果本来就需要完整保存
例如返回 JSON 数组:
def build_api_response(users):
return [
{
"id": user.id,
"name": user.name,
}
for user in users
]
既然最终需要完整列表,就没有必要先创建生成器再转换。
十四、什么时候应该使用生成器表达式?
以下场景通常适合生成器表达式。
1. 数据量很大
valid_rows = (
row
for row in csv_reader
if validate(row)
)
2. 结果只消费一次
total = sum(
order.amount
for order in orders
)
3. 消费者支持迭代器
writer.writerows(
transform(row)
for row in rows
)
4. 需要短路求值
has_invalid_item = any(
not validate(item)
for item in items
)
5. 需要尽快返回首个结果
first_match = next(
(
item
for item in items
if matches(item)
),
None,
)
6. 构建流式处理管道
cleaned = (
clean(item)
for item in source
)
validated = (
item
for item in cleaned
if validate(item)
)
transformed = (
transform(item)
for item in validated
)
十五、不要为了“省内存”盲目使用生成器
生成器并不是越多越好。
下面的代码虽然使用了生成器,但最终仍然转换为列表:
result = list(
transform(item)
for item in items
)
它通常可以直接写成:
result = [
transform(item)
for item in items
]
后者更清晰,而且列表推导式通常针对列表构建进行了优化。
另一个常见问题是惰性链条过长:
result = (
final_transform(item)
for item in (
second_transform(item)
for item in (
first_transform(item)
for item in items
if first_condition(item)
)
if second_condition(item)
)
)
虽然它很“函数式”,但阅读成本过高。
可以使用命名生成器函数改善可读性:
def iter_valid_items(items):
for item in items:
if not first_condition(item):
continue
first_result = first_transform(item)
if not second_condition(first_result):
continue
second_result = second_transform(first_result)
yield final_transform(second_result)
调用:
for result in iter_valid_items(items):
process(result)
普通的 for、if 和 yield 往往比多层嵌套生成器表达式更易维护。
十六、工程选择清单
面对一个数据处理任务,可以依次回答下面几个问题。
问题一:结果是否需要完整保留?
需要完整保留:
result = [
transform(item)
for item in items
]
只需要逐个消费:
result = (
transform(item)
for item in items
)
问题二:结果是否需要多次遍历?
需要多次遍历,优先使用列表。
只遍历一次,可以考虑生成器。
问题三:是否需要索引和切片?
需要:
result[0]
result[-1]
result[:10]
请选择列表。
问题四:数据是否非常大或没有明确终点?
是,则优先考虑生成器。
问题五:消费者是否支持任意可迭代对象?
sum()、any()、all()、max()、min() 和许多文件写入接口都支持迭代器,此时通常不必创建列表。
问题六:惰性求值会不会让资源和异常更难管理?
如果会,应使用列表,或者把逻辑封装为管理资源生命周期的生成器函数。
十七、一张表看懂核心差异
| 对比维度 | 列表推导式 | 生成器表达式 |
|---|---|---|
| 语法 | [] | () |
| 求值方式 | 立即求值 | 惰性求值 |
| 返回对象 | list | generator |
| 内存占用 | 与结果数量相关 | 通常近似恒定 |
| 能否重复遍历 | 可以 | 通常不可以 |
| 是否支持索引 | 支持 | 不支持 |
| 是否支持切片 | 支持 | 不支持 |
是否支持 len() | 支持 | 不支持 |
| 首个结果 | 全部计算后可用 | 可按需立即生成 |
| 无限序列 | 不适合 | 支持 |
| 异常时机 | 创建列表时 | 消费对应元素时 |
| 典型场景 | 小中型结果、重复访问 | 大数据、数据流、单次聚合 |
十八、Python 最佳实践总结
生成器表达式和列表推导式没有绝对的优劣,它们解决的是不同问题。
列表推导式强调“我要得到一组完整结果”:
result = [
transform(item)
for item in items
if condition(item)
]
生成器表达式强调“请按需依次为我产生结果”:
result = (
transform(item)
for item in items
if condition(item)
)
在日常 Python 编程中,可以遵循以下原则:
- 需要完整结果、随机访问或重复遍历时,使用列表推导式;
- 处理大数据、无限序列或单次数据流时,使用生成器表达式;
- 与
sum、any、all、next等函数配合时,优先考虑生成器; - 不要只因为生成器节省内存,就假设它一定更快;
- 注意生成器只能向前消费,使用后可能被耗尽;
- 警惕文件、数据库连接等资源在生成器消费前已经关闭;
- 当逻辑复杂时,使用带
yield的生成器函数,而不是堆叠多层表达式; - 在性能敏感的代码中,用真实数据进行基准测试,不要凭感觉优化。
优秀的 Python 代码,不是永远选择最短的写法,也不是处处追求最低内存,而是在数据规模、执行方式、可读性和维护成本之间找到平衡。
下一次看到这两段代码时:
[value for value in source]
(value for value in source)
不要只看到方括号和圆括号的区别。
前者表达的是:
现在就把所有结果交给我。
后者表达的是:
当我需要时,再给我下一个结果。
理解了这一点,你就真正理解了生成器表达式和列表推导式的核心。
你在实际项目中遇到过生成器被意外耗尽的问题吗?你又在哪些场景中通过生成器显著降低了程序的内存占用?欢迎在评论区分享你的 Python 实战经验,让更多开发者避开那些只有真正踩过才会记住的坑。


556

被折叠的 条评论
为什么被折叠?



