Python 迭代、生成器与上下文管理

# Python 迭代、生成器与上下文管理

本篇目标

理解可迭代对象、迭代器、生成器、装饰器和上下文管理器。这些机制广泛用于数据管道、框架扩展和资源管理。

按需产生数据理解惰性计算封装横切逻辑可靠释放资源

# 先记住一句话

迭代器规定怎样逐个取值,生成器让这种过程更容易编写,上下文管理器保证资源在成功或失败后都被正确释放。

# 可迭代对象、迭代器与生成器

可迭代对象 iterable
  └─ iter() → 迭代器 iterator
                  └─ next() → 下一个值
                              └─ 没有值时抛出 StopIteration
1
2
3
4
from collections.abc import Iterator

# Iterator[list[str]] 表示返回值可被逐次 next(),每次产生一个字符串列表。
def read_batches(items: list[str], size: int) -> Iterator[list[str]]:
    """按批次惰性返回数据,避免一次创建所有批次副本。"""
    if size <= 0:
        raise ValueError("size 必须大于 0")

    # range(起点, 终点, 步长) 依次产生每批的开始下标。
    for start in range(0, len(items), size):
        # yield 暂停函数并保留局部状态,下次继续从这里执行。
        yield items[start : start + size]

# for 会反复向生成器请求下一批,直到生成器结束。
for batch in read_batches(["a", "b", "c"], size=2):
    print(batch)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

生成器的优势是惰性计算:调用时不会立刻生成全部结果,适合处理大文件、分页数据和流式响应。但它通常只能向前消费一次,需要重复遍历时应重新创建生成器。

生成器表达式和生成器函数是什么关系

两者都会创建生成器,并在需要下一个值时继续计算。生成器表达式适合简单的遍历、筛选和转换;逻辑包含多层分支、异常处理或资源清理时,使用带 yield 的生成器函数更清楚。

from collections.abc import Iterator

numbers = [1, 2, 3, 4]

# 简单逻辑可以写成生成器表达式,圆括号不会立即生成完整列表。
compact_result = (number * 2 for number in numbers if number % 2 == 0)

def double_even(values: list[int]) -> Iterator[int]:
    """逐个返回偶数的两倍,与上面的生成器表达式表达相同规则。"""
    for value in values:
        if value % 2 == 0:
            yield value * 2  # 每次产生一个值,并保留函数当前执行位置。

print(list(compact_result))       # [4, 8]
print(list(double_even(numbers))) # [4, 8]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 装饰器是在替换函数

# Callable 描述可调用对象,wraps 保留原函数信息,perf_counter 提供高精度计时。
# ParamSpec 用于保留一个函数的整组参数类型;TypeVar 用于声明可复用的单个类型变量。
from collections.abc import Callable
from functools import wraps
from time import perf_counter
from typing import ParamSpec, TypeVar

# P 代表原函数的整组参数类型,R 代表原函数的返回类型。
P = ParamSpec("P")
R = TypeVar("R")

def timed(func: Callable[P, R]) -> Callable[P, R]:
    """记录同步函数耗时,同时保留原函数的签名和元数据。"""

    # @wraps(func) 让包装后的函数继续保留原函数名称、文档和签名信息。
    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        # *args 收集位置参数,**kwargs 收集带名称的参数,并原样转交给 func。
        started = perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            # finally 保证函数成功或抛出异常时都会记录耗时。
            elapsed_ms = (perf_counter() - started) * 1000
            print(f"{func.__name__} took {elapsed_ms:.2f} ms")

    # 返回包装函数本身,让外部以后调用 wrapper。
    return wrapper

# @timed 等价于 normalize = timed(normalize),用于替换并增强原函数。
@timed
def normalize(text: str) -> str:
    # strip() 去除首尾空白,casefold() 统一大小写;字符串不可变,因此会返回新字符串。
    return text.strip().casefold()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

@timed 等价于 normalize = timed(normalize)。装饰器常用于日志、鉴权、重试和缓存,但顺序会影响行为;高风险业务规则不要隐藏在难以追踪的装饰器链中。

# 上下文管理器保证清理

from pathlib import Path

def load_text(path: Path) -> str:
    # Path.open() 打开文件并返回文件对象,as file 将它绑定到局部变量。
    # with 退出时会关闭文件,即使 read 期间抛出异常也一样。
    with path.open("r", encoding="utf-8") as file:
        return file.read()
1
2
3
4
5
6
7

with 背后调用上下文管理协议。它不仅用于文件,也常用于数据库事务、锁、追踪 Span 和临时资源。

# contextmanager 把包含 yield 的生成器函数转换成上下文管理器。
from contextlib import contextmanager
from collections.abc import Iterator

# 使用装饰器后,operation(...) 的结果可以交给 with 管理。
@contextmanager
def operation(name: str) -> Iterator[None]:
    """演示一个最小操作生命周期;真实项目可在这里创建 Trace Span。"""
    print(f"开始:{name}")
    try:
        # yield 前相当于进入资源,yield 处暂停并执行 with 内部代码。
        yield
    finally:
        # 离开 with 后回到这里,无论成功或异常都会执行清理逻辑。
        print(f"结束:{name}")

# with 自动调用上下文管理器的进入与退出流程。
with operation("建立索引"):
    print("处理中")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 高频面试题与回答

1. 生成器为什么节省内存?参考答案

生成器按需产生下一项,不必先把所有结果放进列表,因此峰值内存通常更低。代价是结果一般只能顺序消费,错误也可能推迟到迭代过程中才出现。

2. 为什么文件操作推荐使用 with?参考答案

with 把获取和释放资源绑定在同一个结构中。无论正常结束还是抛出异常,退出逻辑都会执行,能避免文件句柄、数据库连接或锁被遗漏释放。

3. 可迭代对象和迭代器有什么区别?参考答案

可迭代对象能通过 iter() 取得迭代器;迭代器还通过 next() 保存遍历进度并逐项返回值,结束时抛出 StopIteration。list 可以反复创建新迭代器,而生成器本身就是通常只能向前消费一次的迭代器。

4. 装饰器是在什么时候执行的?参考答案

模块执行到函数定义时,装饰器表达式会被调用,并用返回值替换原函数绑定;真正的包装函数通常在之后调用业务函数时运行。装饰器应使用 functools.wraps 保留名称、文档和签名等元数据,否则调试和框架反射可能受到影响。

# 接下来学什么

下一篇学习 Python 运行时、对象生命周期与 GIL,理解源码怎样执行、对象怎样回收,以及 GIL 对线程并行的实际影响。

# 参考资料

上次更新时间: 2026年09月10日 00:03:52