for 循环的真面目: __iter__/__next__ 协议 — yield 让函数"可暂停", 数据逐个流动, 内存 O(n) 变 O(1)
生成器是"可暂停的函数": 执行到 yield 就地冻结(保存局部变量和位置), next() 时从断点复活。这让数据可以一个一个流过处理链而不是每步都造一份完整副本 —— 同样的管道代码, 内存从 3×O(n) 降到 O(1)。它也是 Python 一切"惰性"与"并发"(asyncio)的地基。
for x in obj = it = iter(obj) 循环 x = next(it) 直到 StopIteration。迭代协议是 Python 最普及的接口, 没有之一。
it = iter([1, 2, 3]) next(it) # → 1 next(it); next(it) # → 2, 3 — for 循环内部就是反复 next next(it) # → StopIteration — for 靠它正常收尾
yield, 调用它不执行任何函数体, 只返回生成器对象; 函数体在首次 next() 时才开跑。
def g(): print("started") # 关键: 调用 g() 时一行都不执行 yield 1 gen = g() # 只创建生成器对象, 函数体未跑 next(gen) # 此刻才打印 started 并产出 1
list(g) 物化(付出内存代价)或重建。
g = (x for x in range(3)) list(g) # → [0, 1, 2] list(g) # → [] — 已耗尽, 静默空转不报错 data = list(make_gen()) # 对策: 要重复用就物化, 或每次重建
len()/切片不可用。
g = (1 / x for x in [1, 0]) # 创建时不报错 next(g) # → 1.0 next(g) # → ZeroDivisionError — 消费时才爆发 len(g) # → TypeError: 生成器没有 len/切片
yield from sub() 委托子生成器逐个透传, 等价于 for + yield, 还会透传 return 值与异常 — 生成器组合的标准写法。
def sub(): yield 1; yield 2 return 3 def outer(): r = yield from sub() # 关键: 逐个透传, 还接住 return 值 yield r list(outer()) # → [1, 2, 3]
g.send(v) 把值注入暂停点(yield 表达式的值), 实现双向通信; g.close() 抛 GeneratorExit 触发 finally — 手动管理生成器生命周期。
def echo(): while True: got = yield # yield 表达式接收 send 注入的值 print(got) g = echo(); next(g) # 关键: 先预激到第一个 yield g.send("hi") # → 打印 hi g.close() # 在暂停点抛 GeneratorExit → 走 finally
islice(惰性切片)、chain(拼接)、groupby(分组) —— 生成器世界的标准库武器, 全部 O(1) 内存。
from itertools import islice, chain, count list(islice(count(1), 3)) # → [1, 2, 3] — 无限流的"切片"仍 O(1) 内存 list(chain([1], [2, 3])) # → [1, 2, 3] — 惰性拼接
统计每个渠道的错误码分布。全量读必 OOM, 生成器管道全程只驻留一行:
def read_lines(path): with open(path, 'rb') as f: for line in f: # 文件对象本身就是惰性迭代器 yield line bad = (l for l in read_lines('app.log') if b'ERROR' in l) parsed = (parse(l) for l in bad) # 生成器链: 元素逐个流过 for (code, n) in Counter(p.channel for p in parsed).most_common(10): print(code, n) # 峰值内存 = 一行 + 一个计数器
把分页查询封装成生成器, 调用方像遍历普通序列一样用, 内部自动翻页:
def iter_page(db, size=500): last_id = 0 while True: rows = db.query("SELECT * FROM t WHERE id > %s ORDER BY id LIMIT %s", (last_id, size)) if not rows: return yield from rows # 委托透传 last_id = rows[-1].id for row in iter_page(db): # 消费方无感: 找到目标即 break, 后面页不查 (早停) ...
对比 fetchall() 一次拉百万行: 生成器版本数据库压力与内存都恒定; 配合早停, 未见过的数据根本不会查询。
from itertools import islice head = islice(huge_stream(), 100) # 惰性取前 100 个, 不会拉全量 sample = islice(stream, 0, None, 100) # 每 100 个抽 1 个做采样监控
10 亿 URL 放不进 set: 分片哈希让"同一条必落同一片", 每片独立去重, 内存恒定:
def shard_urls(urls, n=64): files = [open(f"tmp/shard_{i}.txt", "w") for i in range(n)] try: for u in urls: # urls 是读源文件的生成器 files[hash(u) % n].write(u + "\n") # 同一 URL 恒定落同一片 finally: for f in files: f.close() # 之后逐片 set() 去重 — 单片集合大小 = 总量/64, 可控
自实现 tail -f: 生成器内轮询增量行, 消费方拿到的就是"持续到来的新行":
import time def follow(path): with open(path, "rb") as f: f.seek(0, 2) # 跳到文件末尾 (断点续读改 seek(上次偏移)) while True: line = f.readline() if not line: time.sleep(0.5); continue # 没新内容, 稍后再看 yield line for line in follow("/var/log/app.log"): if b"ERROR" in line: alert(line)
每阶段都是"生成器进、生成器出"的纯函数, 单测喂 iter([...]) 三行数据即可全链路验证:
def parse(lines): yield from (json.loads(l) for l in lines) def keep(evt): yield from (e for e in evt if e["ok"]) def enrich(evt): yield from ({**e, "region": region(e["ip"])} for e in evt) pipeline = lambda src: enrich(keep(parse(src))) # 组合即代码 # 单测: assert list(pipeline(iter([line1, line2]))) == [...]
逐条写库太慢、全量攒怕丢: 生成器把消息流切成固定批量, 吞吐与安全的平衡点:
from itertools import islice def batches(msgs, size=500): while batch := list(islice(msgs, size)): # 取满 500 或耗尽 yield batch for batch in batches(consumer): db.executemany(INSERT_SQL, batch) # 500 行一次落库 consumer.ack(batch) # 批内全部成功才 ack
限速逻辑封进生成器, 业务循环拿不到令牌就等在生成器里, 与业务代码解耦:
import time def token_bucket(rate=100): # 100 QPS tokens, last = 0.0, time.monotonic() while True: now = time.monotonic() tokens = min(rate, tokens + (now - last) * rate) # 按时间补充 last = now if tokens >= 1: tokens -= 1; yield # 发放一枚令牌 else: time.sleep((1 - tokens) / rate) # 差多少睡多少
游标生成器逐行读源表 + islice 分批写目标表, 源不爆、事务日志不膨胀:
def iter_rows(cur): while rows := cur.fetchmany(5000): # 服务端游标, 常驻内存一行 yield from rows src = iter_rows(src_conn.cursor()) while chunk := list(islice(src, 5000)): dst.executemany(INSERT, chunk) # 5000 行/事务, 可断点续跑 mark_progress(len(chunk)) # 进度可观测, 失败从断点重来
fixture 返回生成器, 样本生成开销按需支付, 参数化取之不尽:
import itertools, pytest @pytest.fixture def users(): return (make_user(f"u{i}") for i in itertools.count()) # 无限样本流 def test_topk(users): top10 = list(islice(users, 10)) # 只造 10 个, 不是一万个 assert rank(top10) == sorted(top10, key=score, reverse=True)
list(g) 之后 g 已耗尽, 二次 for 是静默空转(不报错)。正解: 需要多次消费就物化成 list/tuple, 或把生成器做成工厂函数每次重建。
g = gen(); total = sum(g) again = sum(g) # 错: → 0, 二次消费静默空转 data = list(gen()) # 对: 要多次消费就物化或做成工厂
rows = (parse(l) for l in lines) # 错: 坏数据此刻不炸, 消费端才炸 next(rows, None) # 对: 边界处先拉一条尽早校验
len()/切片/反转直接 TypeError。正解: sum(1 for _ in g)(会耗尽)或 itertools.islice; 接口要求序列就别给生成器。
len(g) # 错: → TypeError: object of type 'generator' has no len() g[:10] # 错: 切片同炸 head = islice(g, 10) # 对: 惰性取前 N; 计数用 sum(1 for _ in g)
contextlib.closing(g) 或 with 语句管理, break 前显式 close。
for row in read_huge(): # 错: break 后文件句柄悬到 GC 才关 break with closing(read_huge()) as r: # 对: 退出即 close, 触发生成器 finally for row in r: break
def bad(): # 错: 惰性副作用 — 没消费就没执行, 重试就重复执行 for r in src: db.insert(r); yield r def pure(rs): # 对: 生成器只做纯数据变换 yield from (transform(r) for r in rs) for r in pure(src): db.insert(r) # 副作用放显式消费循环里
any(send(x) for x in batch) 短路即停, 后面的元素根本没发送。正解: 需要全量副作用先物化或拆两步; all/any 只用于纯判断。
ok = any(send(x) for x in batch) # 错: 遇 True 短路, 后面的 x 根本没发送 sent = [send(x) for x in batch] # 对: 全量物化保证都发; 判断与发送拆开
zip(a, b, strict=True) 直接抛错; 老版本先用 len 对齐断言。
list(zip([1, 2, 3], ['a'])) # 错: → [(1, 'a')] — 多出的 2, 3 无声丢失 list(zip([1, 2, 3], ['a'], strict=True)) # 对: → ValueError (3.10+)
islice(g, 10) 之外的部分也已被部分拉取/状态推进, 原 g 再用行为不可预期。正解: 需要多次取段就把源头做成工厂函数, 每次新生成器。
head = islice(g, 10) # 错: g 状态已被推进, 再用行为不可预期 rest = list(g) # 拿到的是第 11 个之后 make = src_gen # 对: 源头做成工厂, 每次取段 islice(make(), 10)
tee 技巧); 单测分层覆盖。
try: yield from upstream() # 错: 5 层管道里炸, 栈只指向消费处 except ParseError as e: raise ParseError(f"stage=parse: {e}") from e # 对: 逐层附加阶段名
with 管理资源 + 消费方显式 close()(触发 GeneratorExit 走 finally)。
def read(path): f = open(path) # 错: 半途 break, f 要等耗尽或 GC 才关 yield from f def read(path): with open(path) as f: # 对: close() 触发 GeneratorExit, with 收尾 yield from f
for row in iter_cursor(cur): # 错: 每秒 1 条 → DB 游标/连接挂几小时 slow_process(row) def batches(cur, n=1000): # 对: 先取批进内存再 yield, 缩短持有时间 while rows := cur.fetchmany(n): yield from rows
def walk(node): # 错: 深树递归 yield from, 每元素穿 N 层帧 yield node.val for c in node.children: yield from walk(c) stack = [root] # 对: 热点层显式栈迭代, yield from 留给浅层 while stack: node = stack.pop(); ...
next(g)(或 g.send(None))跑到第一个 yield, 否则 TypeError。正解: 用 itertools.count 类预置装饰器 @prime 统一预激。
g = echo() g.send("hi") # 错: → TypeError: can't send non-None value to a just-started generator g = echo(); next(g) # 对: 先预激到第一个 yield 再 send
g = coro(); g.send(None) # 错: 老式 send 协程难组合难调试, 别再写 async def coro(): ... # 对: 新代码一律 async/await 原生协程 await coro()
del buf 或置 None 再挂起; 大对象尽量不进生成器作用域。
def pipe(): buf = load_100mb() # 错: buf 跟挂起帧活到耗尽 → 隐形常驻 100MB yield from transform(buf) def pipe(): yield from transform(load_100mb()) # 对: 大对象不进生成器局部; 或用完 del buf
log.debug("items=%s", list(g)) 顺手物化, 正式消费时已空。正解: 调试打印用 islice(g, 3) 采样并接受副作用; 或统一由消费端记数。
log.debug("items=%s", list(g)) # 错: 一行日志把流喝光, 正式消费已空 log.debug("sample=%s", list(islice(g, 3))) # 对: 只采样前 3 条 (也推进状态, 接受副作用)
pipeline = enrich(keep(parse(src))) # 错: 测试只断言构造成功 — 一行都没跑 assert list(pipeline(iter([dirty]))) # 对: 单测必须消费到底, 覆盖脏数据样例
g = src() # 错: 两线程 next 同一个 g, 元素归属混乱 Thread(target=consume, args=(src(),)) # 对: 每线程独立生成器, 或经队列分发
len(list(g)) 把 O(1) 内存变成 O(n)。正解: 计数用 sum(1 for _ in g)(仍会耗尽), 真要反复用就老实物化并接受成本。
n = len(list(g)) # 错: O(1) 内存变 O(n), 10GB 流直接爆 n = sum(1 for _ in g) # 对: 流式计数 (会耗尽); 要反复用就物化并接受成本
def ids(): # 错: while True 无护栏, 进了 for 就是死循环 i = 0 while True: yield i; i += 1 for x in islice(ids(), 1000): ... # 对: 消费端固定上限; 生成器内带 max_iter 防御