(十二)生成器的暂停与恢复
1. 从返回完整列表到逐项 yield
筛选达标记录时,可以先建立一个结果列表。但后面的统计若只是依次读取,也可以找到一项就交出一项:
# 含有 yield 的函数在调用时返回生成器;函数体等取值时才开始执行。
def met_minutes(values, goal=30):
print("开始筛选")
for minutes in values:
print("检查", minutes)
if minutes >= goal:
yield minutes
print("筛选结束")
reader = met_minutes([20, 35, 45])
print("已建立生成器")
print("第一次", next(reader))
print("第二次", next(reader))
print("剩余", list(reader))输出:
已建立生成器
开始筛选
检查 20
检查 35
第一次 35
检查 45
第二次 45
筛选结束
剩余 []
建立 reader 时没有打印“开始筛选”。第一次 next 才进入函数:检查 20 后继续,遇到 35 时执行 yield,把 35 交回并暂停。第二次从这条 yield 之后恢复,再检查 45。第三次继续运行到函数末尾,生成器结束,所以 list 没收集到新项。
return 结束本次函数调用,yield 交出一项并保留继续执行所需的状态。生成器中的 return 或执行到末尾会结束迭代;重新调用 met_minutes(...) 才会建立新的生成器。yield 暂停取值的过程,与后面异步章节的 await 有各自的执行规则。
2. 生成器表达式与多段流水线
第二章的 (number * number for number in values),可以展开成包含循环与 yield 的函数。下面把转换、筛选、求和接起来:
# map 逐项转换,生成器表达式逐项筛选,sum 负责推动整个取值过程。
texts = ["20", "35", "45"]
numbers = map(int, texts)
selected = (minutes for minutes in numbers if minutes >= 30)
print(sum(selected))
print(list(numbers))输出 80 和 []。不是先分别完成三份列表:sum 请求一项,筛选器向 map 请求,map 才转换下一个字符串;遇到未达标的 20,筛选器继续向上游取值。下游拿不到更多结果时,整段处理结束。
逐项交出结果省去了完整的中间结果列表,原来的 texts 列表仍然存在。sorted(reader) 会先收集输入再排序,list(reader) 也会收集全部结果,空间需要按实际保存的数据计算。
3. 用 yield from 依次交出子来源
先用两层循环交出多天的记录:
# 外层选择一天,内层把当天的每项记录逐个交出。
def all_minutes(days):
for records in days:
for minutes in records:
yield minutes
print(list(all_minutes([[20, 35], [], [45]])))当内层只负责依次交出来源中的元素时,可以写成:
# yield from 把当前来源的各项依次交给调用者,再继续外层循环。
def all_minutes(days):
for records in days:
yield from records
print(list(all_minutes([[20, 35], [], [45]])))两段都得到 [20, 35, 45]。第一天交出两项,第二天空列表没有项,随后进入第三天。写 yield records 则每次交出整个子列表,结果是 [[20, 35], [], [45]],数据层级不同。
yield from 还支持向子生成器转交控制与接收其返回结果;本例先掌握逐项交出的读法。带有 yield 的函数可以注解为返回 Iterator[int],表示调用者取得整数迭代器,不是一次拿到完整列表。迭代与生成器