在上一章 節點 (Node) 中,我們學習了 PocketFlow 的基本建構單元——節點,以及它們如何透過 prep
、exec
和 post
三個階段來執行任務。我們也看到 prep
階段會從某處讀取資料,而 post
階段會將結果寫回某處。那個「某處」究竟是什麼呢?這就是本章的主角:共享儲存 (Shared Store)。
想像一下,您和您的團隊正在共同完成一個專案。團隊成員 A 完成了市場調研報告,成員 B 需要根據這份報告來制定行銷策略,而成員 C 則要根據行銷策略設計宣傳材料。他們如何有效地共享資訊呢?他們可能會使用一個共享的雲端硬碟、一個專案管理工具的看板,或甚至只是一塊大家都能看到的白板。
在 PocketFlow 中,不同的節點 (Node) 就像是這些團隊成員,它們各自負責一部分工作。共享儲存 (Shared Store) 就扮演著那個共享白板或公告欄的角色。它是節點與流程之間主要的數據交換媒介。
如果沒有共享儲存,每個節點都會是孤立的,無法將自己的工作成果傳遞給下一個節點,也無法利用先前節點的成果。例如,一個節點下載了數據,另一個節點需要分析這些數據,它們之間必須有一個共通的地方來存放和讀取這些數據。
核心用途:讓數據在節點間順暢流動。
讓我們思考一個簡單的場景:
GetUserNameNode
):負責取得使用者的名稱。GreetUserNode
):負責使用這個名稱來向使用者打招呼。GetUserNameNode
如何將獲取的名稱告訴 GreetUserNode
呢?答案就是透過共享儲存!
共享儲存(Shared Store)通常是一個記憶體內的字典 (dictionary)。您可以把它想像成一個公共的儲物櫃,每個儲物格都有一個標籤(稱為「鍵」,key),裡面存放著物品(稱為「值」,value)。
圖示:共享儲存就像一個團隊的共享白板
prep
方法中,可以從共享儲存讀取執行任務所需的資訊(例如:設定、先前節點的輸出)。post
方法中,可以將自己的工作成果寫回共享儲存,供其他節點使用。這種機制將數據與計算邏輯分離。節點專注於「如何處理數據」,而共享儲存則負責「數據是什麼以及在哪裡」。這使得整個流程設計更加靈活且易於管理。
使用共享儲存非常直觀。因為它本質上是一個 Python 字典,您可以像操作普通字典一樣操作它。
在運行您的節點或流程之前,您通常會先初始化一個空的字典作為共享儲存。
# 一個空的字典,作為我們的共享儲存
shared_data = {}
這個 shared_data
字典將被傳遞給流程中的每一個節點。
post
方法)假設我們有一個節點 GetUserNameNode
,它的任務是獲取使用者名稱並存起來。
from pocketflow import Node # 假設 pocketflow 已安裝
class GetUserNameNode(Node):
def exec(self, prep_res):
# 實際上這裡可能是 input() 或從某處讀取
print("模擬:正在獲取使用者名稱...")
user_name = "小明" # 假設我們獲取到的名稱是 "小明"
return user_name
def post(self, shared, prep_res, exec_res):
# exec_res 就是從 exec 方法返回的 user_name
print(f"GetUserNameNode: 將名稱 '{exec_res}' 存入共享儲存。")
shared["user_name_key"] = exec_res # 將名稱存到 shared store
# 沒有明確 return,預設返回 "default"
在這個例子中:
exec
方法模擬獲取名稱的過程,並返回了 "小明"
。post
方法接收到這個名稱(透過 exec_res
參數)。shared["user_name_key"] = exec_res
。這裡,我們在 shared
字典中創建了一個新的鍵 "user_name_key"
,並將獲取到的使用者名稱作為其值儲存起來。如果我們運行這個節點:
shared_data = {}
get_name_node = GetUserNameNode()
get_name_node.run(shared_data)
print(f"共享儲存的內容:{shared_data}")
輸出將會是:
模擬:正在獲取使用者名稱...
GetUserNameNode: 將名稱 '小明' 存入共享儲存。
共享儲存的內容:{'user_name_key': '小明'}
看!shared_data
字典現在包含了使用者名稱。
prep
方法)現在,我們需要另一個節點 GreetUserNode
來讀取這個名稱並產生問候語。
class GreetUserNode(Node):
def prep(self, shared):
# 從 shared store 讀取 "user_name_key"
name = shared.get("user_name_key", "訪客") # 如果找不到,預設為 "訪客"
print(f"GreetUserNode: 從共享儲存讀取到名稱 '{name}'。")
return name # 將名稱傳遞給 exec
def exec(self, prep_res): # prep_res 是從 prep 方法返回的 name
greeting = f"你好,{prep_res}!歡迎使用 PocketFlow。"
return greeting
def post(self, shared, prep_res, exec_res):
# exec_res 是從 exec 方法返回的 greeting
print(f"GreetUserNode: 將問候語 '{exec_res}' 存入共享儲存。")
shared["greeting_message_key"] = exec_res
在這個節點中:
prep
方法使用 shared.get("user_name_key", "訪客")
來嘗試從共享儲存中讀取之前儲存的名稱。.get()
方法的好處是,如果指定的鍵不存在,它可以返回一個預設值(這裡的 "訪客"
),避免了程式錯誤。exec
方法中用於產生問候語。post
方法再將產生的問候語儲存到共享儲存的 "greeting_message_key"
。讓我們接續上面的例子,運行這個節點:
# shared_data 已經是 {'user_name_key': '小明'}
greet_user_node = GreetUserNode()
greet_user_node.run(shared_data)
print(f"共享儲存最終的內容:{shared_data}")
輸出將會是:
GreetUserNode: 從共享儲存讀取到名稱 '小明'。
GreetUserNode: 將問候語 '你好,小明!歡迎使用 PocketFlow。' 存入共享儲存。
共享儲存最終的內容:{'user_name_key': '小明', 'greeting_message_key': '你好,小明!歡迎使用 PocketFlow。'}
現在,shared_data
包含了兩個節點的成果!這就是共享儲存的魔力——它讓數據在節點之間無縫流動。
雖然共享儲存通常是一個簡單的字典,但在實際應用中,預先規劃好這個字典的結構(即您會使用哪些鍵,以及這些鍵對應的值是什麼類型)是非常重要的。這就像是為您的團隊協作定義好共享文件的格式和內容。
例如,您可以決定:
shared['config']
:儲存一些全域設定。shared['raw_data']['file_path']
:儲存原始文件的路徑。shared['processed_data']['summary']
:儲存處理後的摘要。shared['results']['user_id_123']['score']
:儲存特定用戶的結果。良好的共享儲存結構可以讓您的流程更加清晰和易於維護。
最佳實踐: 優先使用共享儲存來實現關注點分離,將數據綱要與計算邏輯分開!這種方法既靈活又易於管理,能產出更易維護的程式碼。 {: .best-practice}
您可能會好奇,當一個節點修改了 shared
字典,為什麼其他節點(或主程式)能看到這些變動?
在 Python 中,當您將字典(或其他可變對象,如列表)作為參數傳遞給函數(或方法,如節點的 run
, prep
, post
)時,實際上传遞的是該對象在記憶體中的引用(或稱為地址)。
這意味著,run
方法、prep
方法和 post
方法內部操作的 shared
字典,與您在外部創建並傳入的那個 shared_data
字典,實際上是同一個對象。
非程式碼逐步解析:
my_shared_space = {}
。a_node.run(my_shared_space)
。PocketFlow 將 my_shared_space
的引用傳遞給 a_node
。prep
階段:a_node
的 prep(shared)
方法被呼叫。此時,prep
方法中的 shared
參數就是指向 my_shared_space
的引用。如果 prep
讀取 shared['some_key']
,它就是在讀取 my_shared_space['some_key']
。post
階段:a_node
的 post(shared, prep_res, exec_res)
方法被呼叫。同樣,post
方法中的 shared
參數也是指向 my_shared_space
的引用。當 post
執行 shared['new_key'] = 'new_value'
時,它實際上是在修改 my_shared_space
這個原始字典,在其上添加或更新鍵值對。a_node.run()
結束後,您在外部檢查 my_shared_space
,就會發現它已經被 a_node
的 post
方法修改了。序列圖 (Sequence Diagram) 概覽:
這張圖展示了一個節點如何與共享儲存互動:
您可以參考 節點 (Node) 章節中 Node
類別的 _run
方法的簡化版程式碼,可以看到 shared
物件是如何在 prep
和 post
之間傳遞的:
# 位於 pocketflow/__init__.py (為教學目的簡化)
# class BaseNode:
# def _run(self,shared):
# p = self.prep(shared) # 1. shared 被傳入 prep
# e = self._exec(p) # 2. _exec 通常不直接使用 shared
# return self.post(shared,p,e)# 3. shared 再次被傳入 post
因為 shared
是一個可變的字典,並且在這些方法之間傳遞的是它的引用,所以在 post
方法中對 shared
的任何修改都會反映到原始的字典物件上。
PocketFlow 中還有另一種傳遞資訊的方式,稱為「參數 (Params)」。您可能在其他地方看到過它。這裡簡單區分一下:
共享儲存 (Shared Store) (本章重點):
prep()
) 和寫入 (post()
)。參數 (Params):
params
字典,由父流程傳入。參數的鍵和值應為不可變的。對於大多數情況,您應該優先使用共享儲存。它能更好地將數據與計算邏輯分離,使您的流程更清晰。我們會在後續的 批次處理 (Batch) 章節中更詳細地介紹參數 (Params)。
在本章中,我們深入探討了 共享儲存 (Shared Store):
prep
階段從中讀取資訊,在 post
階段將結果寫回。共享儲存是構建 PocketFlow 應用的重要概念。它讓獨立的節點 (Node) 能夠協同工作,共同完成複雜的任務。
現在我們知道了單個節點如何工作,以及它們如何透過共享儲存交換數據。那麼,我們如何將多個節點串聯起來,形成一個有順序的工作流程呢?這就是下一章 流程 (Flow) 將要揭曉的內容。