Context
Context 是 Slyme 在各个 Node 之间传递状态的核心数据结构。它的行为非常类似于 Python 的字典(dict),但有两个关键的不同点:它是层次化的,并且是结构不可变的。
为了方便且类型安全地访问或修改 Context 中的深层嵌套数据,Slyme 引入了 Ref 的概念,它类似于访问字典的“键”,但支持多级路径。
Ref
Ref 是一个不可变的引用对象,用于在 Context 中定位数据。你可以将它理解为一条指向 Context 内部特定位置的路径。
创建 Ref
你可以通过传入一个点分字符串(Dotted string)来创建一个 Ref:
from slyme.context import Ref
# 指向根层级下的 'user'
user_ref = Ref("user")
# 指向嵌套字典中的 'user.profile.name'
name_ref = Ref("user.profile.name")派生 Ref
Ref 提供了 .at() 方法,允许你基于当前路径快速派生出子路径:
profile_ref = Ref("user.profile")
name_ref = profile_ref.at("name") # 等价于 Ref("user.profile.name")INFO
Ref 在内部会缓存哈希值和拆分后的路径片段(parts),因此在执行期频繁使用 Ref 进行查找时具有极高的性能。除此之外,Ref 还可以携带 metadata 和 key_path(用于 PyTree 解析)等高级元数据,以支持命令行参数配置等功能。
Key Path
Ref("a.b.c") 的路径只能放问到 Context 本身的结构化数据,但是对叶子结点无法进一步穿透获取。Slyme 提供了 Key Path 功能,以增强对 Context 结构的访问。可以通过这个例子来理解:
from slyme.utils.pytree import P
# 如果 `hidden_size` 直接存储在 Context 路径上,那么我们可以直接 get 得到
ctx.get(Ref("hidden_size"))
# 如果 `hidden_size` 需要从 Context 存储的 model 的 config 中获取,那么由于 model 本身是普通用户对象,不属于 Context 结构,因此我们可以使用 Key Path
ctx.get(Ref("model", key_path=tuple(P.config.hidden_size)))请注意,其中的 P 是一个特殊的代理对象,支持点属性操作(P.a)、getitem 操作(P[...])和调用操作(P(*args, **kwargs))。上述例子的最终效果是,首先从 Context 的 "model" 路径获取值,然后对其调用 .config.hidden_size,并将最终结果返回。Key Path 功能是对 Node 的进一步解耦,Node 只需要声明自己需要一个 hidden_size 参数,而无需关心这个 hidden_size 是如何计算得到的。
Context
Context 是 Node 运行时的状态容器。基于 Copy-On-Write(写时复制) 机制,每次对 Context 的修改都不会改变原对象,而是返回一个全新的 Context 实例。这种设计从根本上保证了函数式编程的并发安全和状态可追溯性。
创建 Context
你可以通过 update() 方法来初始化一个 Context:
from slyme.context import Context, Ref
ctx = Context().update({
Ref("user.profile.name"): "Alice",
Ref("user.profile.age"): 25,
Ref("status"): "active",
})打印 ctx,你会得到:
Context({
'user': ContextView({
'profile': ContextView({
'name': 'Alice',
'age': 25,
}),
}),
'status': 'active',
})其中,ContextView 指的是 Context 的内部结构化视图,用于表示层次化的 Context 结构。
读取数据
使用 get() 方法并传入 Ref 来获取数据。如果路径不存在,你可以提供一个默认值,否则会抛出 ContextPathError 异常:
# 获取顶层数据
status = ctx.get(Ref("status")) # 'active'
# 获取深层嵌套数据
name = ctx.get(Ref("user.profile.name")) # 'Alice'
# 获取不存在的数据时提供默认值
email = ctx.get(Ref("user.profile.email"), default="unknown")另外,你可以使用 extract() 方法来实现更高级的结构化读取,支持任意嵌套的 Python 字典、列表、元组:
profiles = ctx.extract([
{
"age": Ref("user.profile.age"),
"name": Ref("user.profile.name"),
"status": Ref("status"),
}
])上述例子中,得益于 PyTree 引擎,Slyme 会把嵌套的字典/列表/元组结构中的 Ref 全部解析成对应的值,并且保持原有的结构不变。profiles 值应该是:
[{'age': 25, 'name': 'Alice', 'status': 'active'}]WARNING
请注意,ctx.extract() 方法要求每一个叶子的值都是 Ref 对象,不允许混合普通值,比如 ctx.extract([Ref("status"), 123]) 这样是不允许的。如果想要解析混合的结构,你应该使用更高级的 eval API(详见依赖注入):
from slyme.node.eval import eval_tree
# NOTE: 123 的值不会被解析,保持原样
eval_tree(ctx, [Ref("status"), 123]) # ['active', 123]其他常用的读取方法:
# 检查路径是否存在。
ctx.exists(Ref("user.profile")) # True
ctx.exists(Ref("user.profile.email")) # False
# 列出指定层级下的所有键(类似于字典的 `keys()`)
ctx.keys(Ref("user.profile")) # dict_keys(['name', 'age'])
# 将 Context 递归转换为普通的 Python 字典。
ctx.to_dict() # {'user': {'profile': {'name': 'Alice', 'age': 25}}, 'status': 'active'}修改数据 (Copy-On-Write)
由于 Context 是结构不可变的,所有的修改方法都会返回一个新的 Context 实例。底层的 Copy-On-Write 算法会智能地复用未修改的子树内存,确保修改操作不仅安全而且高效。
# 单个设置 (set)
new_ctx = ctx.set(Ref("status"), "inactive")
# ctx 保持不变,new_ctx 中的 status 变为 inactive
# 批量更新 (update)
new_ctx = ctx.update({
Ref("user.profile.age"): 26,
Ref("user.profile.email"): "alice@example.com"
})
# 删除 (delete)
new_ctx = ctx.delete(Ref("user.profile.age"))原子化事务 (mutate)
如果你需要同时进行复杂的更新和删除操作,可以使用更底层的 mutate() 方法。它保证了所有的修改在一次遍历中原子化完成:
new_ctx = ctx.mutate(
updates={
Ref("user.profile.status"): "verified"
},
drops=[
Ref("status") # 删除顶层 status
]
)比较 Context (diff)
你可以使用 .diff() 方法比较两个 Context 对象之间的差异。它会返回一个 ContextDiff 对象,包含了新增、删除和修改的详细信息。这在调试、状态监控或编写测试用例时非常有用:
new_ctx = ctx.mutate(
updates={
Ref("user.profile.status"): "verified",
Ref("user.profile.name"): "Bob",
},
drops=[
Ref("status")
]
)
diff = new_ctx.diff(ctx)
# 你可以使用 diff.flatten() 将差异展平为一个字典
print(diff.flatten()) # {'status': (<DiffMissing.MARK: 1>, 'active'), 'user.profile.status': ('verified', <DiffMissing.MARK: 1>), 'user.profile.name': ('Bob', 'Alice')}其中,slyme.context.DIFF_MISSING 表示缺失值。diff.flatten() 返回的字典中,tuple 的第一个元素是新值,第二个元素是旧值。这就意味着,DIFF_MISSING 出现在第一个位置,表示值被删除;出现在第二个位置,表示值被新增;否则,表示值被修改。
异步支持
在异步环境(如 @async_node)中,Slyme 提供了 Context 方法的对应异步版本,它们通常以 async_ 开头:
await ctx.async_get(ref)await ctx.async_set(ref, value)await ctx.async_update(updates)await ctx.async_mutate(updates=..., drops=...)await ctx.async_to_dict()
这使得你可以无缝地在同步和异步的 Node 中安全地操作 Context。值得注意的是,Context 本身是本地存储且同步的,异步环境的操作完全由 Context Hook 支持。
Context Hook
为了提供更强大的扩展能力,Context 支持挂载 Hook。通过 Hook,你可以拦截并修改 Context 的读取(Extract)和写入(Mutate)行为。
这在许多高级场景中非常有用,例如:
- 数据转换:在读取时自动反序列化,在写入时自动序列化。
- 外部存储映射:将特定路径的数据映射到外部存储(如 Redis 或数据库),使得 Context 就像一个虚拟的文件系统。
自定义 Hook
你可以通过继承 slyme.context.Hook 并重写相关方法来创建自定义的 Hook:
from typing import Any
from slyme.context import Hook, Context, Ref, ExtractResult, MutateResult
class MyLoggingHook(Hook):
def on_extract(
self,
*,
ctx: Context,
refs: tuple[Ref, ...],
values: tuple[Any, ...],
**kwargs,
) -> ExtractResult:
print(f"[Read] Paths: {[r.path for r in refs]}, Values: {values}")
# 必须返回 ExtractResult,你可以修改 values 来改变实际读取到的值
return ExtractResult(values=values)
def on_mutate(
self,
*,
ctx: Context,
updates: dict[Ref, Any],
drops: set[Ref],
**kwargs
) -> MutateResult:
print(f"[Write] Updates: {updates}, Drops: {drops}")
# 必须返回 MutateResult,你可以修改 updates 或 drops 来改变实际的写入行为
return MutateResult(updates=updates, drops=drops)INFO
对应的异步方法为 on_async_extract 和 on_async_mutate。默认情况下,如果你只实现了同步版本,异步版本会直接调用同步版本。但如果你需要执行真正的异步 I/O(比如查询数据库),你应该重写异步版本的方法。异步支持章节中的方法实际调用的是异步的 Hook 方法。
挂载 Hook
在初始化 Context 时,通过 hook 参数将其实例化并挂载:
ctx = Context(hook=MyLoggingHook())
# 将会触发 on_mutate 打印日志
new_ctx = ctx.set(Ref("status"), "active")
# 将会触发 on_extract 打印日志
status = new_ctx.get(Ref("status"))HookChain
如果你需要同时应用多个 Hook,可以使用 HookChain 将它们组合起来:
from slyme.context import HookChain
chain = HookChain(hooks=(HookA(), HookB(), HookC()))
ctx = Context(hook=chain)在 HookChain 中,Hook 是按顺序执行的:
- 对于
extract,上一个 Hook 修改后的values会作为下一个 Hook 的输入。 - 对于
mutate,上一个 Hook 修改后的updates和drops会作为下一个 Hook 的输入。