学习指南 →

· Albion Gateway College

Python 迭代练习:生成器为什么第二次遍历就空了?

通过逐步消费的原创例子区分可重复遍历的容器、迭代器和生成器,检查耗尽、惰性求值与多遍处理策略。

先区分数据来源与当前读取位置

列表中仍有数据,为什么第二次遍历却没有输出?排查这类问题,要区分“能提供迭代器的对象”与“正在推进的那个迭代器”。列表通常可以反复创建新的迭代器;迭代器则保存当前进度,读取下一项会向前移动。生成器是一种迭代器,不是自动保存全部结果的列表。

需要注意,可迭代对象并不都能重复读取。迭代器本身也可迭代,但 iter(迭代器) 通常返回它自己,因此不会重置进度。只看到 for 语句能够使用某个对象,并不能判断它支持第二遍。写函数约定时,应说明输入只会被消费一次,还是确实需要可重复读取的数据源。

原创例子:四行观察同一个生成器

在 Python 3 中依次执行 g = (x * 3 for x in [2, 5, 8]);print(next(g));print(list(g));print(list(g))。请把这四条语句分别输入,三个输出应为 6、[15, 24]、[]。第一次 next 已经取走首项,第一次 list 只收集余下两项,第二次 list 面对的生成器已耗尽。

这里的 list(g) 并没有复制一个可以重新开始的计算过程,而是消费 g,把当时尚未取得的结果保存到新列表。若先保存 rest = list(g),以后重复遍历 rest 可以再次看到保存的值,但 g 仍然耗尽。不要用第二次 list(g) 为空来推断原始的 [2, 5, 8] 被删除了;原始容器与生成器的位置是两回事。

用两个迭代器验证“独立进度”

另建 values = [4, 7],再执行 a = iter(values) 和 b = iter(values)。依次运行 print(next(a))、print(next(a))、print(next(b)),输出为 4、7、4。列表给了两个独立的读取位置,a 的推进不会使 b 自动跳过首项。这个小实验比只背“列表可重复使用”更容易解释机制。

接着运行 print(iter(a) is a),结果为 True。再给 c = iter(a) 后,c 与 a 指向同一个已推进的迭代器,不是新的列表迭代器。此时 print(next(c, "结束")) 输出“结束”。因此,为同一迭代器换一个变量名,或者再次调用 iter,都不能让已经消费的项目回来。

耗尽如何被正常循环识别

当没有下一项时,直接调用 next(iterator) 会抛出 StopIteration;for 循环把这个信号解释成正常结束,所以第二遍可能只是安静地不执行循环体。为了做小实验,可给 next 提供默认值,例如 next(g, "结束"),这样耗尽时会得到默认值,而非异常。

若默认值也可能是合法数据,就不能靠相等比较可靠判断结束。一般程序可直接用 for 处理完整序列,或使用一个独立的标记对象区分耗尽。不要因为没看到报错就认定第二遍成功执行,更不要用广泛捕获所有异常的办法掩盖读取过程中真正的错误。记录消费了几项,比只记录函数是否返回更有帮助。

惰性求值改变了计算发生的时机

生成器表达式按需求逐项计算,不会在创建时就生成完整结果列表。可试 h = (10 // x for x in [2, 0, 5]):创建 h 本身不会因为后面的零立即失败,第一次 next(h) 返回 5,第二次才在计算 10 // 0 时触发 ZeroDivisionError。这样能把“创建对象”和“执行下一步计算”分开观察。

同时,不要把惰性理解成“所有事情都被推迟”。生成器表达式最左侧 for 子句的数据来源表达式会在创建时求值;后续元素表达式则随推进执行。对有状态的数据来源,还必须考虑读取时数据可能已经变化。因此调试记录应包含创建、首次读取、后续消费三个时点,而不只截取报错那一行。

需要两遍处理时,先选择资源策略

若数据量小且需要多次统计,可在第一次消费之前保存成列表,然后对该列表做多遍处理。代价是存储所有元素;若生成器已经被部分消费,后来再转成列表,只能保留剩余部分,无法补回先前结果。做决定前先估计数据规模,不要对可能无限的来源直接调用 list。

另一种办法是保存“创建新迭代器的函数”,每次需要时重新调用。例如定义 def make_values(): return (x * 3 for x in [2, 5, 8]),两次分别运行 list(make_values()) 都得到 [6, 15, 24]。真实数据来自文件或网络时,重读可能有成本,也可能读到不同内容;可重复执行不等于两次结果必定一致。

自练:先画消费记录,再决定修复方式

新建 q = (x + 1 for x in [1, 3, 5]),随后依次求 next(q)、sum(q)、list(q)。答案依次是 2、10、[];sum 消费的是剩下的 4 与 6。若想得到三项总和 12,同时保留全部三项,应在任何 next 或 sum 之前先把生成器结果完整保存,或重新建立同样的数据来源。

再问自己:调试打印 list(q) 会不会影响后面的业务循环?会,因为打印前的列表构造已经消费它。可迁移的检查顺序是辨认对象类型、找出创建位置、列出所有消费者、标出剩余元素、确认是否要求多遍。使用匿名的短数据即可复现,不要为了获得帮助上传真实日志、访问凭据或同学的个人资料。

参考资料

下一步

如果第二次循环没有结果,可带上数据对象的创建位置、第一次消费位置和预期遍历次数来讨论。练习与计分作业应保持独立。

继续阅读

学院在线咨询

欢迎咨询

你好,欢迎咨询!你是学生、招生代理、求职者,还是想申请成为导师?请告诉我你的咨询方向。

[email protected]