Python 迭代器、生成器与上下文管理:让数据流和资源都有边界
本文目录
Python 的 for 循环看起来很简单,背后却有两个重要的工程概念:数据如何按需提供,以及文件、连接等资源何时释放。理解它们之后,读大文件、组合数据处理步骤和管理一组动态资源都会更清楚。
本文的例子只使用 Python 标准库,适用于 Python 3.9 及以上版本。Iterable 描述“可以开始一次迭代”的对象,Iterator 描述“保存当前进度、可以逐个取值”的对象。两者相关,但不等价。这里的 3.9 是示例语法下限,不是新项目的运行时建议:Python 3.9 已于 2025-10-31 结束支持,不再接收安全更新;请选择官方仍维护的版本,见 Python 3.9.25 发布说明。
Iterable、Iterator 和 generator
列表是可迭代对象;调用 iter(values) 会得到一个迭代器。迭代器实现 __iter__() 和 __next__(),没有下一个值时抛出 StopIteration。普通 for 循环会替你完成这些步骤,所以业务代码通常不需要手动捕获 StopIteration。
生成器函数通过 yield 逐步产出值。调用函数时,函数体还没有开始执行;每次请求下一个值时,它运行到下一个 yield,并保留局部状态。生成器对象是一次性的迭代器,遍历完之后不会自动从头开始。
from collections.abc import Iterable, Iterator
def nonblank_lines(lines: Iterable[str]) -> Iterator[str]:
for line in lines:
cleaned = line.strip()
if cleaned:
yield cleaned
def unique_in_order(values: Iterable[str]) -> Iterator[str]:
seen: set[str] = set()
for value in values:
if value not in seen:
seen.add(value)
yield value
调用 nonblank_lines(source) 只创建生成器,不会立即消费 source。这让它可以把“读取来源”和“清理每一行”分成两步,也避免为每个中间步骤都创建完整列表。unique_in_order 仍需要保存已见值,因此额外内存随唯一值数量增长;惰性不等于零内存。
按需读取,也要看清一次性语义
列表推导式会立即构造列表;生成器表达式和生成器函数按需产值。需要重复遍历、随机访问或排序时,列表可能更合适;处理大文件或长流水线时,才考虑保留迭代器形态。
下面的测试用事件记录证明:创建生成器不会提前读取来源;第一次调用 next() 才开始工作。它也明确展示生成器耗尽后再次遍历会得到空结果。
import unittest
class IteratorGuideTests(unittest.TestCase):
def test_generator_is_lazy_and_one_shot(self):
events: list[str] = []
def source():
events.append("started")
yield " alpha "
yield " "
yield "beta"
values = nonblank_lines(source())
self.assertIs(iter(values), values)
self.assertEqual(events, [])
self.assertEqual(next(values), "alpha")
self.assertEqual(events, ["started"])
self.assertEqual(list(values), ["beta"])
self.assertEqual(list(values), [])
def test_unique_values_keep_first_seen_order(self):
self.assertEqual(
list(unique_in_order(["a", "b", "a", "c", "b"])),
["a", "b", "c"],
)
def test_generator_expression_is_lazy(self):
seen: list[int] = []
values = (seen.append(value) or value * 2 for value in range(3))
self.assertEqual(seen, [])
self.assertEqual(next(values), 0)
self.assertEqual(seen, [0])
真实代码还要考虑生成器的所有权。若生成器内部打开文件或网络资源,而调用方只读取一部分就丢弃它,清理时机就不够显式。对于资源生命周期,优先让调用方用 with 包住实际消费过程;不要把“最终会被垃圾回收”当作释放资源的契约。
用 with 保证资源退出
上下文管理器把进入和退出一段操作的行为配对起来。文件对象本身支持上下文管理协议:离开 with 块时文件会关闭,即使块内抛出异常也一样。自定义上下文管理器适合封装确实需要成对处理的资源或操作。
contextlib.contextmanager 可以用生成器函数实现上下文管理器:yield 前的部分负责进入,yield 后的 finally 负责退出。函数必须恰好 yield 一次。若块内异常发生,异常会在 yield 处重新抛入生成器;只做清理时用 finally,不要意外吞掉异常。
from collections.abc import Iterator
from contextlib import contextmanager
@contextmanager
def activity_scope(events: list[str], name: str) -> Iterator[None]:
events.append(f"start:{name}")
try:
yield
finally:
events.append(f"finish:{name}")
from contextlib import ExitStack
from collections.abc import Iterable, Iterator
from pathlib import Path
from typing import TextIO
@contextmanager
def open_text_files(paths: Iterable[Path]) -> Iterator[list[TextIO]]:
with ExitStack() as stack:
files = [
stack.enter_context(path.open("r", encoding="utf-8"))
for path in paths
]
yield files
使用方式如下:
def print_nonblank_files(paths: Iterable[Path]) -> None:
with open_text_files(paths) as files:
for file in files:
for line in nonblank_lines(file):
print(line)
ExitStack 适合资源数量到运行时才知道的场景。每个成功进入的上下文都会被登记;如果后续打开文件失败,已经打开的文件仍会按退出顺序关闭。这里仍逐行消费文件,没有把所有文件内容同时读进内存。若把行收集进列表,内存消耗仍会随总行数增长。
回归测试:正常退出、异常退出与多文件
把非空行/去重生成器、activity_scope 和 open_text_files 三个实现代码块放在 resource_guide.py;把迭代器测试代码块和下面的资源测试合并保存为 test_resource_guide.py。测试检查延迟执行、异常传播、清理顺序,以及多个文件都在离开上下文后关闭。
import tempfile
import unittest
from pathlib import Path
from resource_guide import activity_scope, nonblank_lines, open_text_files, unique_in_order
class ResourceGuideTests(unittest.TestCase):
def test_context_manager_cleans_up_after_normal_exit(self):
events: list[str] = []
with activity_scope(events, "read"):
events.append("body")
self.assertEqual(events, ["start:read", "body", "finish:read"])
def test_context_manager_cleans_up_and_propagates_errors(self):
events: list[str] = []
with self.assertRaisesRegex(RuntimeError, "broken"):
with activity_scope(events, "write"):
raise RuntimeError("broken")
self.assertEqual(events, ["start:write", "finish:write"])
def test_exit_stack_closes_all_opened_files(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
first = root / "first.txt"
second = root / "second.txt"
first.write_text(" alpha \n", encoding="utf-8")
second.write_text("beta\n", encoding="utf-8")
with open_text_files([first, second]) as files:
self.assertEqual(
[list(nonblank_lines(file)) for file in files],
[["alpha"], ["beta"]],
)
self.assertTrue(all(not file.closed for file in files))
self.assertTrue(all(file.closed for file in files))
if __name__ == "__main__":
unittest.main()
运行:
python -m unittest -v test_resource_guide.py
常见边界
Iterable不保证能够重复遍历:列表每次通常创建新的迭代器,生成器对象则同时是迭代器,保存当前进度并在耗尽后保持耗尽;是否可重放要看具体类型和接口约定。yield让产值按需发生,但如果上游先用read_text()或list(...)读完整份数据,整条流程仍会占用大块内存。- 生成器内部的
finally在正常耗尽、显式关闭或异常展开时执行;不要依赖对象何时被回收来管理文件、锁和连接。 contextmanager中捕获块内异常后不重新抛出,会把异常标记为已处理;只做清理时优先使用try/finally。ExitStack管理动态数量的资源;固定数量的资源直接写多个with项通常更容易读。- 异步资源应使用对应的
async with/asynccontextmanager,不要用同步清理逻辑替代异步关闭。
官方资料
延伸路线见 Python 学习路线,其中包含后续练习方向与相关资料。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。