Python 现代开发学习笔记

(十二)生成器的暂停与恢复

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],表示调用者取得整数迭代器,不是一次拿到完整列表。迭代与生成器

笔记正文与全部题解 · Ctrl / ⌘ K