Python 16:with 退出以后资源归谁
词元生成器只读出第一个 "one" 就停止。随后文件已经关闭;同样的文件在完整读取和消费者抛错两条路径上也关闭。这里的保证并非“生成器迟早会被回收”,而是打开文件的作用域也负责在退出时关闭它,生成器停止时的处理则写在同一个作用域的 finally 中。
09 对生成器提前终止只验证了不再读取后续块,当时根本没有打开文件,不能把那项断言当作资源关闭证据。16 在相同的词频管道外增加真实 UTF-8 文件句柄,并保留原 tasklab 的结果合同。
谁打开,谁确保退出
1 | |
Path.open 创建的文件由内层 with 持有,作用域退出时关闭。生成器 words 交给调用方按需消费;即使消费方只调用一次 next 就离开,外层 finally 也会调用 words.close()。返回源文件对象是为了让当前实验检查 source.closed,并不将“调用方应负责关文件”变成接口规定。Python 3.12 的 with 语句与 contextlib.contextmanager说明了进入、退出和异常传入上下文管理器的规则。
不要把两个动作混为一谈。调用 words.close() 使已启动的词元生成器结束;实际文件的关闭依赖更外层文件的 with 退出。倘若将打开文件的语句挪出该作用域、又只保留生成器关闭,原有 source.closed 断言就不能证明已关闭文件。只依赖 CPython 对无引用对象的及时回收也不够,因为资源所有权应通过明确的控制流表示,而不是靠实现时机碰运气。
三条退出路径与同一任务结果
测试用临时目录创建 "One two one" 文件。完整读取词元得到 "one"、"two"、"one",文件随后关闭,count_file 的词频结果为 {"one": 2, "two": 1},与 submit 同文本后 run 的结果一致。提前退出路径只取第一个词,然后离开 with,文件的 closed 属性为 True。
故障路径在词元读出后由消费者抛 RuntimeError("synthetic consumer error");退出作用域后异常仍向外传播,文件仍关闭。另外写入非法字节 b"\xff",文件迭代时抛 UnicodeDecodeError,源文件在异常之后同样关闭。这一失败发生在读取/解码层,不应谎称词频任务已经成功或结果已经提交。本章并没有把文件内容直接作为可跨进程保存的任务输入;17 的 CLI 与 21 的存储仍需处理入口和持久化问题。
在 examples/python-language/ 下运行:
1 | |
演示与累计测试均以 0 退出,累计 50 条测试。演示记录 early_closed: true、failure_closed: true、错误消息与基线结果相等;额外测试实际断言完整读取、提前停止、消费者异常和非法编码四种关闭路径。examples/python-language/evidence/16/20261007-resources/ 保存本次解释器、临时输入、真实命令、原始输出及断言。未启动进程强杀实验;强制终止不能推断 Python 的 finally 曾执行。
预测题
在 with open_word_file(path) 内只建立 words 变量、不调用 next,然后立即退出。需要依赖垃圾回收才会关闭文件吗?
答案:不需要。生成器即使尚未开始,文件的 with 退出路径仍应关闭已打开句柄;words.close() 对未启动的生成器也不改变文件作用域的责任。本次测试已覆盖正常、提前及异常的已打开资源路径;“建立但零次读取”是可补充的独立变式,不能冒称已测。
变更题
把文件打开移到函数外部,仅让生成器负责在自己结束时关闭文件,调用方拿到第一个词后丢弃生成器。哪些断言能抓到风险?
答案:需要保留文件对象的引用,在退出调用方作用域后立即检查 source.closed,并在正常、抛错、提前停止三条路径重复检查。不应只观察某次解释器恰好回收了生成器,更不能以测试进程退出后操作系统最终回收句柄替代函数级关闭证明。生产实现没有采用该变更。
下一阶段从脚本进入 CLI、模块、SQLite 与安装。已有业务动作仍是提交、执行、查询、取消;跨进程状态尚未实现。系列导航只连接实际正文。




