Python 生成器、迭代器与装饰器
更新时间:2026-08-29。本文回答:
yield到底是什么?生成器为什么省内存?装饰器如何在不动原函数的情况下加功能?
一、迭代器协议:for 循环背后的约定
Python 的 for x in obj 能遍历任何"可迭代对象",靠的是两条协议:
- 可迭代(iterable):实现
__iter__(),返回一个迭代器。 - 迭代器(iterator):实现
__next__(),每次返回下一个值,耗尽时抛StopIteration。
python
class Count:
def __init__(self, n): self.n = n; self.i = 0
def __iter__(self): return self
def __next__(self):
if self.i >= self.n: raise StopIteration
v = self.i; self.i += 1; return v
for x in Count(3):
print(x) # 0 1 2核心结论:迭代器把"遍历逻辑"和"数据容器"解耦——你不必一次性把所有元素放进内存。
二、生成器:用 yield 写迭代器
手写迭代器很啰嗦,生成器(generator)用 yield 让函数"暂停/恢复",自动拥有迭代器协议:
python
def fib(n):
a, b = 0, 1
for _ in range(n):
yield a # 每次到这里"暂停返回",下次从下一行继续
a, b = b, a + b
for v in fib(5):
print(v) # 0 1 1 2 3| 维度 | 列表(一次性) | 生成器(惰性) |
|---|---|---|
| 内存 | 全部元素入内存 | 只保存当前状态 |
| 适用 | 数据量小、需随机访问 | 大流、无限序列、管道处理 |
| 速度 | 建表有开销 | 按需计算,省内存 |
工程价值:读大日志文件、解析 perf 输出流时,用生成器逐行 yield,内存占用是常数级——和本站处理观测数据的场景天然契合。
三、装饰器:不动原函数加功能
装饰器是高阶函数,接收函数、返回新函数。本质是闭包(closure)——内层函数"记住"了外层作用域的变量:
python
import time
def timed(f):
def wrap(*args, **kw):
t = time.time()
result = f(*args, **kw)
print(f"{f.__name__}: {time.time()-t:.3f}s")
return result
return wrap
@timed
def compute():
return sum(range(10_000_000))@timed等价于compute = timed(compute)。- 常见用途:计时、缓存(
functools.lru_cache)、鉴权、重试、日志——横切关注点的统一注入点。
四、与性能剖析主线的衔接
| Python 机制 | 本站对应 | 衔接文档 |
|---|---|---|
| 生成器惰性 | 缓存与程序性能 | 流式处理降低内存压力 |
| 闭包/装饰器 | 函数式范式 | 高阶函数跨语言同源 |
lru_cache 缓存 | CPU 缓存组织 | 空间换时间,减少重复计算 |
| 装饰器计时 | perf 观测方法论 | Python 层热点定位入口 |
五、常见坑
- 生成器只能遍历一次:遍历完再
for一遍会得到空,需重新构造或用list()物化。 yield from委托:yield from sub_gen把迭代委托给子生成器,写递归生成器更清晰。- 装饰器吞掉元信息:用
functools.wraps(f)保留原函数的__name__/__doc__,否则日志和调试会困惑。
一句话总结
生成器用 yield 把"一次性建表"变成"按需产出"(省内存),装饰器用闭包把"横切逻辑"统一注入(省重复)——两者都是 Python "写得少、跑得对"的代表。
继续:GIL、多进程与 asyncio → 性能优化路径。