
tee 通过共享缓存链表将单次输入分流为多个独立迭代器,内存开销取决于快慢分支的消费步调差距。
处理 5GB 的支付对账文件时,程序需要同时进行账务匹配与差异审计。若为了让两段逻辑复用数据而先执行 list(),进程会在大文件场景下触发内存溢出(OOM);若让两个消费者轮流读取同一个生成器,后执行的分支则会因为游标已耗尽而丢失全部记录。
这引出一个真实的工程矛盾:输入流只能单向顺序读取一次,但下游多个步骤必须各自看到完整且一致的序列。
掌握 itertools.tee 的关键,在于理清它如何通过受控的共享缓冲来兼顾复用与内存开销。面对这类需求,首要解决的正是:同一条输入只能读一次时,如何让多个业务步骤各自消费。
tee 把“谁先读谁拿走”的单一消费权,变成少量业务步骤各自可顺序读取的视图。它适合输入不能倒带、又不值得重复获取的场合。
批量 CSV 导入中,字段校验与错误行审计都要查看同一行。把文件先转成 list,两者可以重复遍历;但文件越大,全部行越久地常驻内存,最终可能内存耗尽(Out of Memory,OOM)。用 tee 分出校验和审计分支,并在每个批次内都处理完,可避免预先保存整个文件。
支付对账中,账务匹配和差异报表需要同一笔流水。若各自读取对象存储并解析文本,上游 I/O 和解析会重复发生。tee 让两项处理共享一次顺序读取;但报表生成若持续慢于匹配,尚未被报表读取的流水仍会占用缓存。
客服工单导出同样受慢分支和缓存增长的约束。JSON Lines(每行一个 JSON 对象的文本流)可一次解析后,分别生成脱敏下载文件和内部质量统计。这里的下载文件应在受控批处理内产生。若直接写入不受控的慢网络连接,网络回压会使下载分支长期落后,缓存无法稳定回收。
直接让两个步骤交替调用同一个迭代器并不是分发。一方取得元素后,迭代器的位置已经前进,另一方只能看到后续元素,业务结果会漏记录。tee 创建的是多个独立迭代器,使它们能观察同一顺序的输入。
常被误以为 tee 会自动并行处理,其实它不负责线程、调度、失败隔离或重试。它解决“避免重复读取”,不是零内存广播。任一分支落后越远,系统为其保留的元素越多;内存占用主要取决于分支间的消费差距。
使用 tee 的前提是分支较少、消费速度接近,并能在当前进程内完成。跨进程分发、持久化重放,或消费者可能长期落后时,应使用消息队列、落盘文件或数据库。
itertools.tee 的核心机制在于,它将上游迭代器看作一条只能向前走的传送带,并为每个分支设置独立的阅读书签。这种设计确保了多个分支能看到相同的序列,同时避免了重复读取上游数据。
当我们调用 a, b = tee(source, 2) 时,系统并不会立刻从 source 读取任何元素。只有当某个分支(例如 a)执行 next(a) 时,系统才会按需从上游 source 拉取下一个元素。
这个元素会被放入一个共享缓冲中。如果 a 是第一个到达该元素的分支,它将元素放入缓冲并记录自己的书签位置。当慢分支 b 随后读取时,它直接从缓冲中取得该元素的引用,而不是再次触发上游 source 的读取。
书签机制解释了为什么两个分支都能读到相同顺序的数据。然而,这个缓冲不是无限的。慢分支的书签位置决定了系统必须保留多少数据。落后的书签迫使系统保留中间货物,直到所有分支都越过该位置,元素才可能被释放。
边界:两个分支拿到的是同一个对象的引用,而不是深拷贝。如果元素本身是可变对象(Mutable Object),一方修改对象内容,另一方会观察到修改后的结果。
tee 的内存占用完全取决于分支间的消费差距。因此,生产代码应让两个分支在有限批次内共同推进,而不是先把一个分支完整消费完再处理另一个。
若先执行 list(validation_branch),校验分支会持续拉取上游,而审计分支的书签停留在起点。此时,共享缓存为了等待审计分支,将保留几乎全部输入,重新退化为全量加载,可能导致内存溢出(OOM)。
虽然 tee 的第二个参数是生成分支数量,而不是缓存大小,且没有用于限制缓存上限的公开参数,但我们可以通过外部批处理逻辑来约束最大消费差距。
以下代码示例使用 islice 确保两个分支同步消费一个批次,将最大缓存量近似约束在批次大小范围内:
这种批次协调的代价是代码不能完全独立地任意消费两个分支。若业务天然需要一个分支比另一个分支晚数小时或数天处理,应改用可持久化的中间存储(如数据库或消息队列),而不是依赖 tee 的进程内缓存。
itertools.tee 能够复制迭代能力而不复制全部数据,其实现基础在于共享缓存数据块,并让每个分支独立记录自己在序列中的当前位置。
在 CPython 中(适用于 3.12/3.13),tee 机制的核心是 teedataobject(分段缓存)。只有当某个分支首次请求新元素时,上游迭代器才会被调用一次。该元素随后被写入共享的 teedataobject 数据块。这些数据块是链式结构,用于存储已消费但仍被至少一个分支需要的元素。
快分支继续前进时会不断拉取上游数据并填充新的缓存块。每个分支迭代器只维护一个指向当前正在读取位置的指针。
正在渲染 Mermaid 图表...
代价:只有当所有仍存活的分支都越过旧数据块后,该数据块才有机会被垃圾回收释放。因此,慢分支决定了内存保留窗口。该结构避免了重复调用上游,但每个被滞后分支需要的元素都必须保存在内存中。如果元素本身很大、分支滞后差距大或分支长期不消费,内存溢出(OOM)风险将显著增加。
tee 的核心适用范围是受控的、同步或可协调推进的单进程迭代。它不应被当作多线程扇出队列。
Python 官方文档明确指出,同一个 tee 返回的迭代器在多线程中同时使用时,可能抛出 RuntimeError。这是因为 tee 内部的指针和缓存管理没有针对并发访问进行同步保护。需要并发消费时,应使用带同步语义的 queue.Queue 等机制。
决策规则:
反例:不要用 tee 给 HTTP 流式响应复制“实时直播”和“离线归档”两条路径。网络回压(Network Backpressure)会让直播分支的速度不可控。任一方落后都可能造成缓存持续增长,最终导致内存溢出(OOM)。
tee 通过共享缓存链表将单次输入分流为多个独立迭代器,内存开销取决于快慢分支的消费步调差距。
在单进程内将不可倒带的流式数据分发给多个消费者。无需重复拉取上游输入,也不需要预先全量落盘。
适用于少量分支(通常 2~3 个)、在单进程内执行、各分支消费速率高度同步(如校验与计费同步进行)的不可倒带流式数据处理。