← 返回题目列表

Python 列表推导式和生成器表达式有什么区别?

高频 中等 第 11 / 22 题 更新于 2026/07/25
Python列表推导式生成器表达式惰性计算

简化版

列表推导式用 [],会立即生成完整列表;生成器表达式用 (),不会一次性生成所有结果,而是按需惰性产出。数据量小、需要多次遍历时用列表推导式;数据量大、只迭代一次时生成器表达式更省内存。

详细版

列表推导式:

squares = [x * x for x in range(5)]
print(squares)  # [0, 1, 4, 9, 16]

生成器表达式:

squares = (x * x for x in range(5))
print(squares)       # <generator object ...>
print(list(squares)) # [0, 1, 4, 9, 16]

区别:

对比项列表推导式生成器表达式
语法[...](...)
计算方式立即计算惰性计算
返回值listgenerator
内存一次性保存所有结果按需产生,更省内存
是否可重复遍历可以通常只能消费一次

例子:

total = sum(x * x for x in range(10_000_000))

这里用生成器表达式,不需要先创建一个巨大列表。

完整版教学

一、先抓住本质:一个造结果,一个造流水线

列表推导式的目标是“现在就把结果列表造出来”,所以表达式执行结束后,内存里已经有了完整的 list。生成器表达式的目标是“记住怎么算,下次要值时再算”,所以它返回的是一个 generator 迭代器,里面保存了当前位置和计算逻辑。这个差异会直接影响内存占用、是否能重复遍历、异常出现时机和调试方式。

nums = range(5)
lst = [x * x for x in nums]
gen = (x * x for x in nums)

print(lst)       # [0, 1, 4, 9, 16]
print(next(gen)) # 0
print(next(gen)) # 1

可以把列表推导式想成一次性把 5 个盒子摆在桌面上;生成器表达式则像一个水龙头,每拧一次才流出一个值。假设每个元素最终约占 28 字节,100 万个整数光元素对象就可能是几十 MB 级别,还没算列表指针数组;而生成器本身只保存迭代状态,峰值内存通常小得多。

二、列表推导式为什么适合构造可复用结果

result = []
for x in range(5):
    if x % 2 == 0:
        result.append(x * x)

列表推导式:

result = [x * x for x in range(5) if x % 2 == 0]

它简洁、表达力强,适合把一个可迭代对象转换成列表。这里的重点不是“写得短”,而是结果确实需要作为列表继续使用:比如要取长度、按下标访问、排序、多次遍历、传给只接受列表的旧接口。因为结果已经在内存中,len(result) 是 O(1),result[2] 可以直接下标访问。

names = ["tom", "jerry", "spike"]
upper_names = [name.upper() for name in names]

print(len(upper_names)) # 3
print(upper_names[1])   # JERRY

但不要为了“显得 Pythonic”把推导式写成谜语。如果出现两层以上嵌套、复杂条件、或者中间还要记录日志,普通 for 循环往往更清楚。面试里说“列表推导式可读性更好”要加前提:逻辑简单、目标是构造列表。

三、生成器表达式为什么适合流式处理

生成器表达式不会立刻计算全部元素:

gen = (x * x for x in range(3))

print(next(gen))  # 0
print(next(gen))  # 1
print(next(gen))  # 4

它像一条流水线,需要一个值时才算一个值。sum(x * x for x in range(10_000_000)) 不会先创建一个包含 1000 万个平方数的列表,而是每次取一个 x * x 加到累计值里。若写成 sum([x * x for x in range(10_000_000)]),中间列表会先占一大块内存,然后再被 sum 消费,内存峰值明显更高。

场景更适合原因
只求总和、最大值、是否存在生成器表达式消费一次即可,不必保存全部
结果要排序、切片、复用列表推导式需要完整序列
逐行处理大日志生成器表达式文件流式读取,降低峰值内存
小列表转换并展示列表推导式简洁且成本可控

例如:

with open("access.log", encoding="utf-8") as f:
    error_lines = (line for line in f if "ERROR" in line)
    for line in error_lines:
        print(line)

这里不会一次性把所有错误日志读入内存。

四、生成器为什么通常只能消费一次

生成器是有状态的迭代器,被遍历后就前进了。

gen = (x for x in range(3))

print(list(gen))  # [0, 1, 2]
print(list(gen))  # []

第二次为空,因为第一次已经消费完。生成器内部有一个“当前位置”,每次 next() 都向前走一步,走到尽头后抛出 StopIteration,后续遍历就没有元素了。这个特性很适合流式处理,但如果你打算重复使用结果,就应该提前转成列表。

流程可以这样理解:

创建 gen -> next 得到 0 -> next 得到 1 -> next 得到 2 -> StopIteration
                ↑             ↑             ↑
             状态前进       状态前进       状态前进到末尾

如果确实要多次遍历,可以写 nums = list(gen) 把结果存下来,或者重新创建一个新的生成器表达式。不要把同一个生成器传给两个消费方,否则第一个消费方可能已经把数据“吃完”。

五、语法细节和异常时机也不同

当生成器表达式作为函数唯一参数时,外层括号可以省略:

sum((x * x for x in range(10)))
sum(x * x for x in range(10))

两者等价,第二种更常见。

但如果还有其他参数,生成器表达式自己的括号不能省:

print(*(x for x in range(3)), sep=",")

还有一个细节:列表推导式会立即执行内部表达式,所以异常会在创建列表时出现;生成器表达式通常在真正迭代时才执行内部表达式。这个差异会影响调试和资源释放。

lst = [10 // x for x in [2, 1, 0]]   # 这里立刻 ZeroDivisionError

gen = (10 // x for x in [2, 1, 0])   # 创建 gen 时还不报错
next(gen)  # 5
next(gen)  # 10
next(gen)  # 这里才 ZeroDivisionError

易错点:生成器表达式“创建成功”不代表里面的逻辑已经跑过;错误、数据库访问、文件读取等副作用,往往会推迟到消费阶段才发生。

六、推导式不要滥用副作用

不推荐这样写:

[print(x) for x in range(3)]

列表推导式的目的是构造列表,而不是为了执行副作用。上面会创建一个 [None, None, None] 的临时列表,语义不清楚。需要打印时直接用循环:

for x in range(3):
    print(x)

如果推导式只是为了副作用,还会制造无意义的中间结果。比如 [print(x) for x in range(3)] 实际得到 [None, None, None],只是没人用这个列表。面试时可以说:推导式适合表达“映射 + 过滤 + 构造结果”,副作用明显的逻辑用普通循环,更利于维护。

七、常见误区与追问

  • 误区:生成器表达式一定比列表推导式快。 它主要省内存,速度要看场景;小数据量下列表推导式可能因为连续内存和一次性执行更直接,反而不慢。
  • 误区:圆括号表达式都是生成器。 (x) 只是普通表达式,(x for x in data) 才是生成器表达式;单元素元组还要写 (x,)
  • 误区:生成器可以像列表一样随便取下标。 generator 没有随机访问能力,想取第 100 个元素只能迭代过去,或者先转成列表。
  • 追问:为什么 sum(x*x for x in nums) 外层括号能省? 因为生成器表达式是函数唯一实参时,语法允许省略它自己的括号;有其他参数时不能省。
  • 追问:生成器表达式会捕获变量吗? 会按闭包和迭代规则保存计算所需环境,循环变量在表达式自己的局部作用域中,不会像 Python 2 的列表推导式那样泄漏到外层。
  • 追问:列表推导式能替代 map/filter 吗? 很多场景可以,而且可读性更好;但如果已有现成函数且要惰性流水线,mapfilter 或生成器表达式也合适。

八、加强记忆

把这题记成“方括号是成品仓库,圆括号是生产流水线”:列表推导式一次性生产完整列表,适合复用、下标访问和排序;生成器表达式只保存生产规则和当前位置,适合大数据、流式处理和只消费一次。回答时再补上两个边界:生成器通常不能重复遍历,异常和副作用可能推迟到消费阶段发生。