marimo 单元执行机制全解:反应式执行、静态分析与运行时配置
【免费下载链接】marimoA reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All in a modern, AI-native editor.项目地址: https://gitcode.com/GitHub_Trending/ma/marimo
导读
marimo 是一个"反应式笔记本"(reactive notebook)框架:它与 Jupyter 最大的区别在于单元(cell)的执行顺序和触发方式。本文以仓库自带的入门示例 examples/running_cells/basics.py 为核心骨架,结合 docs/guides/reactivity.md 反应式执行指南与 docs/guides/configuration/runtime_configuration.md 运行时配置指南,深入讲解 marimo 单元的执行规则、底层静态分析原理,以及如何通过设置面板将默认的自动执行切换为惰性(lazy)执行。读完本文,你将掌握 marimo 笔记本"运行一个单元、自动级联更新所有依赖单元"的完整机理,并能针对昂贵的笔记本正确配置运行时行为。
一、示例概览:一个最简反应式笔记本
仓库中的入门示例文档 docs/examples/running_cells/basics.md 通过 marimo 文档系统的marimo-embed-file指令,把可运行示例直接嵌入页面:
/// marimo-embed-file size: xlarge mode: edit show-chrome: true filepath: examples/running_cells/basics.py ///这意味着该文档的主体就是 examples/running_cells/basics.py。打开它可以看到一个 marimo 笔记本以纯 Python 形式存储时的真实样貌——这正是 marimo 的核心卖点之一:笔记本即纯 Python 文件,可纳入 git 版本管理。
import marimo __generated_with = "0.19.7" app = marimo.App() @app.cell def _(): import marimo as mo return (mo,) @app.cell(hide_code=True) def _(mo): mo.md(""" marimo knows how your cells are related, and can automatically update outputs like a spreadsheet. This eliminates hidden state and hidden bugs, accelerates data exploration, and makes it possible for marimo to run your notebooks as scripts and web apps. For expensive notebooks, you can [turn this behavior off](https://link.gitcode.com/i/cb26b562e89430a2edf85cc7680c1863) via the notebook footer. Try updating the values of variables below and see what happens! You can also try deleting a cell. """) return @app.cell def _(): x = 0 return (x,) @app.cell def _(): y = 1 return @app.cell def _(x): x return if __name__ == "__main__": app.run()可以观察到 marimo 笔记本文件的几个典型特征:
- 每个单元对应一个以
@app.cell装饰的函数,函数体就是该单元的 Python 代码; - 单元之间的依赖通过函数参数声明:例如最后一个单元定义
def _(x),表示它读取了全局变量x; - 函数通过
return语句把单元定义的全局变量显式暴露给其他单元(如return (x,)); __generated_with记录了生成该文件的 marimo 版本(此处为 0.19.7);- 文件末尾的
if __name__ == "__main__": app.run()让笔记本可以像普通脚本一样执行。
示例的意图非常直白:x = 0、y = 1两个单元定义变量,第三个单元引用x,第四个单元用mo.md渲染说明文字。修改x的值,引用x的单元会立刻自动重新执行;删除定义x的单元,引用它的单元也会被重新执行或标记为过期。接下来我们从源码层面解释这一切是如何发生的。
二、反应式执行:运行一个单元,自动更新所有依赖单元
2.1 运行时规则
marimo 反应式执行模型的核心规则只有一条,在 docs/guides/reactivity.md 中被明确为 "Runtime Rule":
运行一个单元时,marimo 会自动运行所有"引用"(reference)了该单元所"定义"(define)的全局变量的其他单元。
这与电子表格的行为一致:修改数据源,依赖它的公式自动重算。由此带来的直接收益是:
- 消除隐藏状态(hidden state):程序状态始终与代码同步,不会出现"变量被修改过但页面还显示旧值"的错位;
- 消除隐藏 bug:依赖关系由代码自动推导,无需手工维护"重新运行全部"的纪律;
- 确定性的执行顺序:单元执行顺序由依赖关系决定,与页面上的摆放位置无关;
- 这一模型同时支撑了 marimo 的交互式元素(interactivity)、以 Web 应用运行(
marimo run)和以脚本运行(python nb.py)三大能力。
2.2 底层实现:静态分析构建依赖图
marimo 之所以能做到这一点,是因为它在运行代码之前先对每个单元做一次静态分析。相关源码位于 marimo/_ast/visitor.py:marimo 用 Python 标准库的ast模块解析每个单元,在不执行代码的情况下提取两类信息:
- references:单元读取但没有定义的全局变量;
- definitions:单元定义的全局变量(含导入的名字,因为导入也是一种变量定义)。
随后,marimo 以单元为节点、以变量依赖为边,构造一张有向无环图(DAG):若单元 B 引用了单元 A 定义的某个变量,则在 A 与 B 之间建立一条边。运行时(见 marimo/_runtime/runtime.py)在执行某个单元后,沿着 DAG 找到其后代(descendants)并标记为待执行,从而触发级联重算。该文件中的lazy()属性即对应运行时的惰性模式判断:
def lazy(self) -> bool: return self.reactive_execution_mode == "lazy"执行模式在autorun与lazy之间切换,具体见下文第四节。
2.3 可视化依赖图
要直观理解自己笔记本里的依赖关系,marimo 提供了多种可视化工具,详见 docs/guides/editor_features/dataflow.md:
- 依赖图视图(Graph view):以节点和连线展示单元间的数据流;
- 小地图(Minimap):在长笔记本中快速导航;
- 反应式引用高亮(Reactive reference highlighting):选中一个变量时高亮所有引用它的位置。
下图展示了反应式执行的效果——修改一个变量,依赖它的所有输出自动刷新:
三、执行顺序与"无隐藏状态"的保证
3.1 执行顺序与页面顺序无关
在 marimo 中,单元在页面上的排列顺序不影响执行顺序,执行顺序只由变量依赖关系决定。这意味着你可以把辅助函数、"附录"放在笔记本底部,把最重要的输出放在顶部,代码组织完全服务于叙事需要。
3.2 删除单元即删除其变量
marimo 有一条与 Jupyter 截然不同的规则:删除一个单元,会同时从程序内存中删除它定义的所有全局变量,而不再仅仅是一个"移除代码"的动作。之前引用这些变量的单元会被自动重新执行并被失效处理(invalidated),或在惰性模式下被标记为过期(stale)。这正是"无隐藏状态"的关键实现——传统笔记本中最常见的 bug 来源,恰恰是"某个变量来自一个早已被删掉的单元"。
3.3 变量突变(mutation)不被跟踪
marimo 的依赖分析基于"变量定义/引用"的静态关系,不会跟踪对象内部的运行时突变。例如:
my_list.append(42)不会触发引用my_list的其他单元重跑;my_object.value = 42同样不会触发反应式更新。
因此最佳实践是:不要在 A 单元定义变量、在 B 单元突变它。如果确实需要突变(比如给 DataFrame 加一列),应在定义该变量的同一单元内完成,或者干脆创建新变量,例如:
# 推荐:创建新变量 l = [1] extended_list = l + [2] # 不推荐:跨单元突变 l = [1] l.append(2) # 引用 l 的单元不会自动更新marimo 不在运行时跟踪突变并非疏漏,而是在 Python 中可靠地跟踪突变本质上不可行,强行反应反而会造成意外的大量重算。
3.4 中断执行
点击停止按钮可以中断正在运行的单元。在类 Unix 系统上,中断还会波及内核进程组中的子进程;若希望某个子进程在中断后继续存活,可以用subprocess.Popen(..., start_new_session=True)把它放到新的会话中启动。中断一个单元也会取消其排队中依赖单元的后续执行。
四、全局变量命名唯一性与临时变量
4.1 每个全局变量只能由一个单元定义
marimo 要求每个全局变量只能被一个单元定义,这是保持"代码与输出一致"的前提。注意这里的"变量"是广义的:函数、类、导入的名字都属于变量。该规则也客观上鼓励你保持笔记本中全局变量数量精简。
4.2 用下划线前缀创建局部变量
以单个下划线开头的变量(如_x)是该单元的局部变量:其他单元无法读取它,多个单元可以安全地复用相同的局部变量名。这适用于中间计算结果。
一个容易被忽略的细节:导入的名字也是变量,所以import numpy as _np这样的下划线别名导入同样只对本单元可见。当被导入符号本身以下划线开头、又希望跨单元使用时,需要为其起一个不带下划线的别名,例如from ibis import _ as d。
4.3 用函数封装临时变量
如果单元里大部分变量都是临时性的,逐个加下划线很繁琐。此时推荐把临时变量封装进函数,只返回真正需要暴露的结果:
def _(): import matplotlib.pyplot as plt fig, ax = plt.subplots() ax.plot([1, 2]) return ax _()这里plt、fig、ax都不会进入全局命名空间。
4.4 内存管理
由于变量名必须唯一,不能通过"重新赋值"来释放内存。需要释放内存时应使用del运算符,或借助函数封装让临时变量随函数结束而消亡。更多技巧见 docs/guides/expensive_notebooks.md。
五、配置运行时:从自动执行到惰性执行
默认情况下 marimo 采用autorun(自动执行)模式;但并非所有场景都适合自动执行,尤其是包含昂贵单元(长耗时计算、副作用)的笔记本。通过笔记本设置菜单(notebook settings)或笔记本页脚,你可以配置 marimo 何时、如何运行单元,详见 docs/guides/configuration/runtime_configuration.md。
5.1 On startup:启动时是否自动运行
控制用marimo edit打开笔记本时是否自动执行全部单元。需要注意:用marimo run以应用方式分享笔记本时,该设置不生效(应用启动总是会运行以产出初始输出)。
5.2 On cell change:切换为惰性执行(lazy)
在笔记本设置菜单中把"On cell change" 设为 "lazy",即可关闭"单元运行或 UI 交互后自动执行其后代单元"的默认行为。惰性模式的行为特征如下:
- 运行一个单元后,受影响的单元只被标记为 stale(过期),而不会自动执行;
- 当你运行一个具有 stale 祖先的单元时,marimo 会连带运行这些祖先,确保你的单元不会用到过期输入;
- 你可以随时点击"运行全部过期单元"按钮(或使用快捷键)批量刷新。
从源码实现看,autorun与lazy两种模式在 marimo/_runtime/runtime.py 中分流处理:autorun 模式下运行单元后立即调度其后代执行,而 lazy 模式下只做 stale 标记,仅在显式运行或需要输出时才回溯执行祖先。惰性模式同样对marimo run应用模式无效。
什么时候该用惰性模式?当笔记本中存在昂贵单元、且你只想在第一个单元上反复迭代时,惰性模式能显著减少无谓的级联重算。仓库的入门示例也在mo.md说明文字中提示:对于昂贵笔记本,可以通过页脚关闭自动更新行为。
5.3 模块自动重载(On module change)
启用模块自动重载后,当你编辑被笔记本导入的 Python 文件时,marimo 会基于静态分析只重跑受影响的那部分单元,且重载是递归的——被导入模块再导入的模块的修改同样会被跟踪。重载分为两种模式:
- autorun:模块变更后自动重跑受影响单元;
- lazy:只把受影响单元标记为 stale,由你决定何时重跑。
这一特性支持的典型工作流是:在普通 Python 模块中开发复杂逻辑,用 marimo 笔记本充当编排这些逻辑的 DAG 或主脚本。
5.4 通过 pyproject.toml 持久化运行时配置
部分运行时配置可以写入项目的pyproject.toml,随仓库一起版本化。例如启用全笔记本单元自动缓存:
[tool.marimo.runtime] cache_cells = true默认情况下,marimo 只缓存显式用mo.cache或mo.persistent_cache装饰的函数/代码块;设置cache_cells = true后,marimo 会尝试缓存笔记本中执行的每个单元,详见 docs/api/caching.md。
又如配置 Python 路径,让笔记本能导入指定目录下的模块(等价于把目录加到sys.path头部):
[tool.marimo.runtime] pythonpath = ["project/src"]marimo 默认不会向 Python 路径添加任何额外目录,以保证marimo edit nb.py与python nb.py的行为一致。相比路径操作,更推荐的做法是把共享代码做成 Python 包并声明为项目依赖(如uv add --dev marimo后uv run marimo edit notebook.py)。此外还支持通过dotenv = [".env", ".env.testing"]配置加载环境变量文件(.env文件中的变量会显示在 UI 的数据库创建界面中)。
六、禁用单元:局部编辑不触发级联
有时你想修改笔记本的一部分,却不想触发其依赖单元的自动执行——例如依赖单元耗时很长,而你只想快速迭代前面的单元。此时可以禁用(disable)单元:被禁用的单元及其依赖单元都会被阻止运行。
重新启用单元时,如果在其禁用期间有祖先单元运行过,marimo 会自动重跑该单元,保证结果一致:
七、三种运行形态:编辑、应用、脚本
反应式执行模型让同一个.py文件可以三种方式运行:
marimo edit notebook.py # 以可编辑笔记本打开(默认自动执行) marimo run notebook.py # 以只读 Web 应用运行(隐藏代码、纵向拼接输出) python notebook.py # 作为纯 Python 脚本执行其中"作为脚本执行"之所以可行,正是因为 examples/running_cells/basics.py 中if __name__ == "__main__": app.run()的存在;脚本模式还支持命令行参数传递,详见 docs/guides/scripts.md。而反应式执行保证无论以哪种形态运行,输出都与代码严格一致。
八、继续深入
本文聚焦"单元如何被执行"这一主题,仓库还提供了围绕该主题的系列示例与进阶指南:
- 运行相关示例:async_await.md(异步函数)、debugging.md(调试器)、multiple_definitions.md(多重定义错误)、memory_cache.md(内存缓存)、persistent_cache.md(持久化缓存)、stop.md(停止执行)、run_button.md(按钮触发执行)、refresh.md(定时刷新);
- 核心概念速览:docs/getting_started/key_concepts.md;
- 依赖图与可视化工具:docs/guides/editor_features/dataflow.md;
- 昂贵笔记本优化:docs/guides/expensive_notebooks.md;
- 运行时底层实现:marimo/_runtime/runtime.py(autorun/lazy 分流)、marimo/_ast/visitor.py(静态分析提取 references/definitions)。
掌握"单元执行"这一层,就抓住了 marimo 区别于传统笔记本的根基:代码、输出与程序状态三者永远一致,隐藏状态与隐藏 bug 无处藏身。
【免费下载链接】marimoA reactive notebook for Python — run reproducible experiments, query with SQL, execute as a script, deploy as an app, and version with git. Stored as pure Python. All in a modern, AI-native editor.项目地址: https://gitcode.com/GitHub_Trending/ma/marimo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考