Python graphlib 依赖排序:字典里要写前置任务,完成后才能放行下一步

10-01 3阅读

先把“依赖谁”写清楚

制作一份报告时,抓取资料后才能校验,校验通过后才能计算图表与排版,最后才能打包。若只是按函数在文件里的位置执行,流程一长就难以看清先后关系。标准库 graphlib 可以把这些约束转成可执行顺序,让依赖关系成为可检查的数据。它解决顺序条件,任务怎样运行仍由程序安排。

最容易写反的是字典方向。键表示当前任务,值表示它的全部前置任务。因此 package 对应 charts 和 pages,含义是打包要等这两项完成。前置节点即使没有单独作为键出现,也会被自动加入;正式配置最好把节点写全,并额外核对名称,免得拼写错误被当成新任务。

把就绪与完成分成两步

下面的独立脚本在 Python 3.12.14 上运行,使用的 graphlib 从 Python 3.9 起提供。prepare 先固定图并检查循环;get_ready 返回当前已经满足前置条件的一批节点;done 则报告哪些节点真正完成。这个接口允许把一批节点派给多个工作者,但并不替你启动线程或进程。

实验将同一批就绪节点排序后打印,只是为了让展示稳定。charts 与 pages 之间没有依赖,谁先执行都符合图的约束。不能把拓扑排序的一次输出误读为唯一顺序,也不要为了让输出好看而给独立任务随便添加依赖,那会降低本可利用的并行度。

Python graphlib 依赖排序:字典里要写前置任务,完成后才能放行下一步

AI概念示意图:有向依赖从一个入口分成两路再汇合,下方独立圆环表示循环依赖。图片用于解释概念,不是运行截图。

from graphlib import TopologicalSorter, CycleError

graph = {
    "fetch": (),
    "check": ("fetch",),
    "charts": ("check",),
    "pages": ("check",),
    "package": ("charts", "pages"),
}
ts = TopologicalSorter(graph)
ts.prepare()
finished = set()
batches = []
while ts.is_active():
    ready = sorted(ts.get_ready())
    assert ready
    assert ts.get_ready() == ()
    for node in ready:
        assert set(graph[node]) <= finished
        finished.add(node)  # 本例用断言模拟成功完成
    batches.append(ready)
    ts.done(*ready)
print(batches)
assert batches == [["fetch"], ["check"],
                   ["charts", "pages"], ["package"]]

try:
    TopologicalSorter({"draft": ("review",),
                       "review": ("draft",)}).prepare()
except CycleError as exc:
    cycle = exc.args[1]
    assert cycle[0] == cycle[-1]
    assert set(cycle) == {"draft", "review"}
    print("cycle detected:", sorted(set(cycle)))
else:
    raise AssertionError("expected a cycle")

读懂一轮轮放行的结果

输出共有四批:fetch、check、并列的 charts 与 pages、最后的 package。第二次立刻调用 get_ready 得到空元组,因为第一批节点已经交给调用方,排序器不会把它们重复发放。它们尚未 done 时,依赖它们的节点也不会提前出现。这个细节正好能检查调度器是否把“拿到任务”混同于“任务完成”。

真实任务失败后,应由业务决定重试、终止或记录失败。不要为了让循环继续而对失败节点调用 done,否则后续任务会把失败输入当成已就绪。本例所有工作同步完成,所以每轮必有可执行项;异步执行时 get_ready 可能暂时为空,但仍有已派发任务正在运行,需要等待完成事件,不能直接判定死锁。

循环不是换个顺序就能解决

第二张图让初稿等待审阅、审阅又等待初稿,prepare 会抛出 CycleError。异常携带一条首尾相同的循环路径;若图中有多条循环,它不保证列出全部。输出把涉及的节点排序,是为了验证这个小例子的成员,而不是声称异常总按某个固定方向返回。

修复时要回到真实流程,确认审阅究竟需要初稿还是更早的提纲,再调整依赖。对于只需要一次线性顺序的小脚本,也可使用 static_order;需要跟踪并行任务完成状态时,再用本例的分阶段接口。依赖图应描述必要条件,资源数量、重试次数和超时仍要另外设计。把实际完成事件与节点名称一一对应,才能避免误放行。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。