Python 列表生成式与生成器:从立即计算到按需生成
列表生成式和生成器表达式可以使用几乎相同的推导语法。不同的是,生成式使用方括号,会立即创建完整列表;生成器使用圆括号,返回一个生成器对象。这个看似很小的变化,会进一步影响计算时机、内存占用和数据能否重复使用。
squares_list = [x * x for x in range(5)]squares_generator = (x * x for x in range(5))print(squares_list)print(squares_generator)
第一行直接得到 [0, 1, 4, 9, 16],第二行只创建了生成器,打印时看到的是类似 <generator object <genexpr> at ...> 的对象信息。
同一段推导,两种计算方式
列表生成式与生成器表达式都可以描述“从一组输入中逐个取值,经过计算后产生结果”。区别在于结果如何保存。
生成式方括号:立即创建完整列表
执行列表生成式时,Python 会遍历输入,把每次计算得到的值放进一个新列表。表达式结束后,所有元素都已经在列表中:
squares = [x * x for x in range(5)]print(len(squares))print(squares[2])print(list(squares))
列表支持 len()、数字索引和重复遍历。这里的代价也很直接:无论后面实际使用多少个元素,创建时都会先把完整结果算出来并保存在内存中。
生成器圆括号:返回生成器对象
把最外层方括号换成圆括号,得到的是生成器表达式:
squares = (x * x for x in range(5))print(next(squares))print(next(squares))
两次 next() 依次得到 0 和 1。生成器没有提前保存后面的全部平方数,而是在请求下一个值时继续计算。
两种写法没有统一的优先级。结果较小,而且后面需要索引、求长度或重复遍历时,列表更方便;数据量较大、可能提前停止,或者只需要把结果交给下一个处理步骤时,生成器更合适。
列表生成式:把转换和筛选写进表达式
列表生成式的基本结构,可以从普通循环一步步对应出来。
基本结构:表达式、for 与输入序列
先用循环生成 1 到 5 的平方:
squares = []for x in range(1, 6): squares.append(x * x)
循环里的三个角色很明确:range(1, 6) 提供输入,for x 逐个取值,x * x 决定输出。把它们放进列表生成式,顺序变成“输出表达式在前,循环在后”:
squares = [x * x for x in range(1, 6)]
两段代码都会得到 [1, 4, 9, 16, 25]。列表生成式适合表达简单、没有副作用的转换;但是如果一行代码已经需要多个括号来控制逻辑,表达式很复杂,其实更适合使用普通循环来创建列表。
for 后面的 if:过滤元素
在 for 后面添加 if,可以决定当前输入是否进入结果列表:
even_squares = [ x * x for x in range(1, 11) if x % 2 == 0]print(even_squares)
输出为 [4, 16, 36, 64, 100]。执行时先从 range() 取得 x,再检查 x % 2 == 0;条件成立才计算并收集前面的 x * x。
这个位置的 if 是过滤条件,因此不能在后面直接添加 else:
[x for x in range(1, 6) if x % 2 == 0 else 0]
这段代码会抛出 SyntaxError。过滤只回答“当前元素要不要保留”,被排除的元素不会再产生替代值。
for 前面的 if...else:决定输出值
如果每个输入都要保留,只是根据条件产生不同结果,就要把条件表达式放到 for 前面:
numbers = [ x if x % 2 == 0 else -x for x in range(1, 6)]print(numbers)
输出为 [-1, 2, -3, 4, -5]。这里的 x if ... else -x 必须为每个 x 计算出一个值,所以 else 不能省略。
两种写法可以用一句执行顺序区分:for 后面的 if 负责筛选输入,for 前面的 if...else 负责计算输出。
多层循环:按照嵌套顺序组合数据
列表生成式可以继续追加 for 子句,其顺序与普通嵌套循环一致:
combinations = [ left + right for left in "ABC" for right in "XY"]print(combinations)
输出为 ['AX', 'AY', 'BX', 'BY', 'CX', 'CY']。这里得到的是两个集合的笛卡尔积:外层先固定一个 left,内层再遍历所有 right。
循环变量也可以直接解包。例如,遍历字典的 items() 时,每一项都是一个键值对:
scores = {"Alice": 95, "Bob": 88}items = [f"{name}={score}" for name, score in scores.items()]print(items)
在 Python 3.7 及之后,字典保持插入顺序,因此这里输出 ['Alice=95', 'Bob=88']。
生成器表达式:需要一个再计算一个
列表生成式负责描述转换规则,生成器表达式在此基础上改变了结果的产生方式。它返回的是生成器迭代器,可以被 next() 和 for 驱动。
从 [] 换成 ()
下面两段代码使用相同的转换规则:
numbers = range(1, 6)squares_list = [x * x for x in numbers]squares_generator = (x * x for x in numbers)
squares_list 创建时已经完成遍历;squares_generator 则等到外部请求数据时,才计算下一个 x * x。
“惰性计算”也有一个细节:生成器表达式最左侧 for 后面的可迭代对象会在创建生成器时求值,其他表达式通常等到取值时再执行。因此,它不是把整行代码无条件推迟到第一次 next(),而是把逐项计算的主要工作延后。
当生成器表达式是函数调用中唯一的位置参数时,可以省略它自己的那层圆括号:
total = sum(x * x for x in range(1, 6))print(total)
sum() 会逐个消费生成器产生的值,最后得到 55,中间不需要先创建平方数列表。
next、for 与 StopIteration
next() 展示了生成器最直接的消费方式:
squares = (x * x for x in range(3))print(next(squares))print(next(squares))print(next(squares))print(next(squares))
前三次分别得到 0、1、4。第四次已经没有数据可取,Python 会抛出 StopIteration。
完整遍历时通常交给 for:
squares = (x * x for x in range(3))for value in squares: print(value)
for 会在内部不断请求下一个值,并在遇到 StopIteration 时正常结束循环。代码无需手动捕获这个异常,但 next() 仍然适合只取一个值或手动控制消费节奏的情况。
一次性消费与耗尽状态
生成器保存的是当前执行状态,不是一份可以反复读取的结果集合。走到结尾以后,它会一直保持耗尽状态:
squares = (x * x for x in range(3))print(list(squares))print(list(squares))
第一次输出 [0, 1, 4],同时把生成器消费完;第二次只能得到空列表 []。如果需要再次遍历,就要重新创建生成器;如果需要随机访问、求长度或者保留结果,则应当使用列表或主动执行 list(generator)。
生成器函数:yield 保存执行现场
生成器表达式适合简单推导。计算过程包含循环状态、多个分支或递推关系时,可以把它写成包含 yield 的生成器函数。
调用函数:先得到生成器对象
下面的函数在三个位置打印日志,并产生两个值:
def trace_values(): print("start") yield 1 print("resume") yield 3 print("end")values = trace_values()print("created")
执行这段代码只会打印 created。调用 trace_values() 创建了生成器对象,函数体还没有开始运行,所以 start 暂时不会出现。
第一次 next:运行到 yield
继续对同一个对象调用 next():
print(next(values))
这时函数从开头执行,先打印 start,再执行 yield 1。yield 把数值 1 交给调用方,同时暂停函数;局部变量、指令位置和当前执行状态都会被保留下来。
这里没有像普通 return 那样结束整个函数。生成器只是暂时把控制权交回调用方,等待下一次请求。
再次 next:从暂停位置继续
第二次调用 next(values) 时,执行会从上一个 yield 后面恢复:
print(next(values))
程序先打印 resume,然后通过 yield 3 产生第二个值并再次暂停。第三次调用时只剩下 print("end"),函数执行到末尾,随后用 StopIteration 表示生成器已经结束。
这套“运行到 yield—保存状态—从原位置恢复”的过程,使生成器函数可以自然地表达递推计算:
def fibonacci(count): index, current, next_value = 0, 0, 1 while index < count: yield next_value current, next_value = next_value, current + next_value index += 1
每次需要新值时,函数利用保留下来的 current、next_value 和 index 继续计算,无需提前保存整个数列。
return:结束生成器
生成器函数执行到末尾,或者遇到 return,都会结束迭代。如果写成 return "done",这个值会放在 StopIteration.value 中;普通 for 循环会自动处理 StopIteration,不会把 "done" 当成一个普通元素输出。
还有一个容易忽略的地方:每次调用生成器函数都会创建新对象。下面两行都从第一个 yield 开始,因为它们操作的是两个不同的生成器:
print(next(trace_values()))print(next(trace_values()))
如果希望继续前一次的执行位置,需要先保存对象,再反复对同一个对象调用 next() 或使用 for 遍历。
容易写错的三个地方
列表生成式和生成器的常见错误,都可以回到它们各自的执行位置来判断:
- •
for 后面的 if 是过滤条件,不能直接带 else;要为每个输入选择输出值,应把完整的 if...else 放到 for 前面。 - •
next(generator_function()) 每次都会创建新生成器,无法沿用上一次暂停位置;需要先把对象保存到变量中。 - • 生成器是一次性迭代对象,消费完成后不会自动复位;需要重复遍历时,应重新创建生成器或保存为列表。
这三种错误表面上分别属于语法、函数调用和遍历,背后对应的是三个不同问题:当前代码是在筛选输入、创建对象,还是消费已有对象。
[]、() 与 yield 怎么选
三种形式可以按照结果规模和使用方式来选择:
| | |
|---|
[expression for ...] | | |
(expression for ...) | | |
| | |
列表生成式和生成器表达式都应该保持简洁。当转换规则开始承担复杂控制流时,把逻辑放回普通循环或生成器函数,通常比继续压缩语法更容易维护。