
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 圖表...
代價:只有當所有仍存活的分支都越過舊資料區塊後,該資料區塊才有機會被垃圾回收機制(GC)釋放。因此,慢分支決定了記憶體保留範圍。這種結構可以避免重複呼叫上游,但每個落後分支所需的元素都必須保留在記憶體中。如果元素本身很大、分支之間的延遲差距很大,或分支長時間沒有消費資料,記憶體不足(OOM)的風險就會顯著增加。
tee 主要適用於受控、同步,或能夠協調推進的單一行程迭代。它不應被視為多執行緒的扇出佇列。
Python 官方文件明確指出,同一個 tee 所回傳的迭代器若在多執行緒中同時使用,可能會擲出 RuntimeError。這是因為 tee 內部的指標與緩衝區管理並未針對並行存取提供同步保護。若需要並行消費,應改用具備同步語意的 queue.Queue 等機制。
選擇原則:
反例:不要使用 tee將 HTTP 串流回應複製成「即時直播」與「離線封存」兩條路徑。網路回壓(Network Backpressure)會讓直播分支的處理速度變得不可控。任一分支落後,都可能造成緩衝區持續成長,最終導致記憶體不足(OOM)。
tee 透過共享快取鏈結串列,將單次輸入分流為多個獨立迭代器;記憶體開銷取決於快、慢分支的消費步調落差。
在單一程式內,將不可倒帶的串流資料分發給多個消費者。無需重複拉取上游輸入,也不需要事先將全部資料寫入磁碟。
適用於分支數量少(通常 2~3 個)、在單一程序內執行、且各分支消費速率高度同步(例如校驗與計費同步進行)的不可倒帶串流資料處理情境。