
列表生成式和生成器表达式可以使用几乎相同的推导语法。不同的是,生成式使用方括号,会立即创建完整列表;生成器使用圆括号,返回一个生成器对象。这个看似很小的变化,会进一步影响计算时机、内存占用和数据能否重复使用。
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。生成器没有提前保存后面的全部平方数,而是在请求下一个值时继续计算。
|
对比项 |
列表生成式 |
生成器表达式 |
|
外层语法 |
[] |
() |
|
创建结果 |
list |
生成器迭代器 |
|
计算方式 |
立即产生完整结果 |
按需产生下一个值 |
|
数字索引、len() |
支持 |
不支持 |
|
重复遍历 |
支持 |
耗尽后不能重新开始 |
两种写法没有统一的优先级。结果较小,而且后面需要索引、求长度或重复遍历时,列表更方便;数据量较大、可能提前停止,或者只需要把结果交给下一个处理步骤时,生成器更合适。
列表生成式:把转换和筛选写进表达式
列表生成式的基本结构,可以从普通循环一步步对应出来。
基本结构:表达式、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 ...] |
完整 list |
结果较小,需要索引、长度或重复使用 |
|
(expression for ...) |
生成器迭代器 |
简单推导,按需消费,可能提前结束 |
|
包含 yield 的函数 |
生成器迭代器 |
多步计算、递推、分支或需要保存局部状态 |
列表生成式和生成器表达式都应该保持简洁。当转换规则开始承担复杂控制流时,把逻辑放回普通循环或生成器函数,通常比继续压缩语法更容易维护。
以上就是“Python 列表生成式与生成器:从立即计算到按需生成”的详细内容,想要了解更多Python教程欢迎持续关注编程学习网。
扫码二维码 获取免费视频学习资料

- 本文固定链接: http://phpxs.com/post/14420/
- 转载请注明:转载必须在正文中标注并保留原文链接
- 扫码: 扫上方二维码获取免费视频资料