Python 08:for 循环向对象请求了什么
同一份任务 ID 集合第一次遍历得到三个 ID,第二次遍历仍能得到三个;把第一次生成的迭代器留在手里,再遍历一次却得到空列表。数据没有被删除。两次操作只是取迭代器的方式不同:可迭代对象能提供新遍历,已经耗尽的那个迭代器不会自动倒回起点。
前一章把四种业务操作统一分派。这里仍使用同一进程的 tasklab,为任务列表增加读取 ID 的接口;不把遍历后的列表当成自动更新的数据库视图。
集合与一次遍历不是同一个东西
tasklab.task_ids(store) 返回当时任务 ID 组成的元组。元组可以多次交给 iter;每次获得独立的迭代器,逐次向它调用 next,直到 StopIteration。Python 3.12 的迭代器教程展示了 for 如何向对象请求迭代器,以及迭代器怎样逐项提供值。
1 | |
实测 first 为 task-0001,rest 为 task-0002 和 task-0003,exhausted 是空列表,而 again 又是三项。for 不会为“一个已耗尽的迭代器”另行构造游标;传入一个可重读的元组才会创建新迭代器。如果把 iterator 直接传给两个消费者,第二个消费者只能接着第一个尚未读完的位置,而不是从头开始。
集合再读与重新取快照也不同
实验随后提交 task-0004。旧 collection 仍含前三项,重新调用 task_ids(store) 则得到四项。原因不是元组同步读取了存储,而是 task_ids 每次根据当时的任务表创建一份新的不可变 ID 快照。这里读取的是 ID 列表,不泄漏可变任务字典;想读取单个任务仍须走 query 的快照边界。
这套接口也限定了性能预期:tuple(store["tasks"]) 会为当前所有任务 ID 分配空间,并非无需内存的流式查询。09 将词频输入按需产出词元,才开始讨论生成器的挂起与恢复。未来 SQLite 仓储取代进程内字典时,必须保留“这一次调用获得快照”的契约或显式修改接口,不能无声变成边读边更新的游标。
从 examples/python-language/ 运行:
1 | |
演示和累计回归均以状态码 0 退出。第二条命令运行 26 条测试,其中新增两条分别对比耗尽与再读、旧快照与新提交后的快照。examples/python-language/evidence/08/20261007-iterators/ 保存真实输出、退出码与输入环境。没有文件输入、没有数据库连接,所以这些断言不能证明资源退出时关闭了句柄。
预测题
只消耗 next(iterator) 一次,剩下两项由另一个函数执行 list(iterator),第一个函数再次读取 list(iterator) 会得到什么?若它改为 list(collection) 呢?
答案:共享同一个迭代器时,第二个函数先把余下项读完,第一次函数再次读取就得到 []。重新对元组 collection 请求迭代器,则从快照的第一项开始读三项。实验中已执行先读取一项、消耗余下两项和再读两种路径。
变更题
若要求“提交第四项后,先前得到的 collection 也能看到第四项”,只把 tuple(...) 改成 store["tasks"].keys() 会改变哪些可观察条件?
答案:字典视图会反映新键,不再满足旧集合快照不变的断言;若遍历时结构改变,还需检查字典迭代器对增删键的行为和错误。现有 test_snapshot_does_not_change_after_new_submission 应先失败,才能明确这是接口变更,而不是无关优化。此变更未实施。
下一章讨论 yield:它在调用时不执行完整函数体,而且提前停止消费时,资源可能比预期活得更久。系列导航只链接真实页面。


