marimo 输出机制完全指南:Cell Outputs、Console Outputs 与 mo.output API 实战
2026/9/13 8:18:35 网站建设 项目流程

marimo 输出机制完全指南:Cell Outputs、Console Outputs 与 mo.output API 实战

【免费下载链接】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 中,单元格(Cell)既可以产生可视化输出(Cell Outputs),也可以产生写入stdout/stderr控制台输出(Console Outputs),两者共同构成了 Notebook 界面与应用视图的呈现基础。本文基于仓库中的 docs/api/outputs.md 官方 API 文档,结合marimo/_runtime/output/_output.pymarimo/_runtime/capture.py等源码实现与 tests/_runtime/output/test_output.py 测试用例,系统讲解输出区的行为规则、mo.output系列 API 的底层原理、控制台输出的捕获与重定向,以及mo.show_code()mo.inspect()的应用场景。读完本文,你将能精确控制每个单元格的最终呈现内容,并为「以 App 形态分享 Notebook」做好输出编排。

一、Cell Outputs:单元格的可视化输出

1.1 输出区的两种存在形态

marimo 的每个单元格都可以拥有一个可视化输出(Cell Outputs),其呈现方式随运行形态不同而变化:

  • 编辑模式下:输出显示在单元格代码的上方(代码相当于输出的"图注")。用户也可以在设置中将输出配置为显示在单元格下方。
  • 应用模式下(如通过marimo run运行 Notebook 时):应用界面本质上就是所有单元格输出的排列组合,代码默认隐藏,输出成为唯一可见的内容。

单元格输出的默认来源是该单元格的最后一个表达式(非None)。例如在 examples/outputs/cell_output.py 中,单元格以字符串字面量"Hello, world!"结尾,该字符串即为单元格的可视化输出。

1.2 编程式输出:mo.output.replace()mo.output.append()

除了"最后一个表达式"这一隐式规则,marimo 还提供了一组显式 API,让你能够在单元格执行过程中任意时刻创建、替换、追加输出,适合循环中逐步构建图表、多阶段渲染等场景。核心 API 定义于 marimo/_runtime/output/_output.py:

API行为
mo.output.replace(value)替换单元格现有输出(若有)为value
mo.output.append(value)在现有输出之后追加value,多个输出按垂直堆叠排列
mo.output.clear()清空单元格输出(等价于replace(None)
mo.output.replace_at_index(value, idx)替换输出列表中索引idx处的对象

从源码看,replace()的完整流程是:先调用output.clear()清空该单元格的输出栈,再对value执行formatting.as_html(value)格式化并追加到输出栈,最后通过write_internal()将渲染结果广播给前端(marimo/_runtime/output/_output.py);而append()不清空已有内容,直接将新对象追加到输出栈尾部(marimo/_runtime/output/_output.py)。

追加的多个输出最终会以垂直堆叠(vstack)的方式渲染——这正是CellOutputList.stack()的实现逻辑(marimo/_runtime/cell_output_list.py)。该容器还通过threading.RLock()保证线程安全,因此多线程代码中并发调用输出 API 也是安全的。

一个典型的应用是循环内增量构建输出:

import marimo as mo # 逐步追加,最终垂直堆叠显示多个输出 for i in range(3): mo.output.append(mo.md(f"**第 {i} 轮结果**")) # 之后仍可用 replace 覆盖为单一输出 mo.output.replace(mo.md("最终结论"))

1.3replace_at_index的边界语义

mo.output.replace_at_index(value, idx)用于替换输出栈中指定位置的对象。需要注意其边界行为(marimo/_runtime/cell_output_list.py):

  • idx小于当前输出长度,则替换该位置的对象;
  • idx等于当前输出长度,则等价于一次append
  • idx大于当前输出长度,会抛出IndexError

这一语义在 tests/_runtime/output/test_output.py 的测试中被验证。

1.4 关键警告:最后一个表达式会替换已有输出

原文档给出了一个极易踩坑的规则:

以非None表达式结尾的单元格,等价于对该表达式调用mo.output.replace()——它会替换此前已经写入的所有输出。

这意味着,如果你在单元格中先mo.output.append()了若干内容,最后一行却又是一个普通表达式(如df.head()),那么此前追加的内容会被最后这个表达式覆盖。若希望保留并追加而非替换,请将最后一个表达式用mo.output.append(...)包裹:

import marimo as mo mo.output.append(mo.md("处理过程日志...")) mo.output.append(mo.md("统计摘要...")) # 错误示范:最后的表达式会替换掉上面的两次 append # df.head() # 正确示范:继续追加,保留已有输出 mo.output.append(df.head())

从实现层面看,单元格执行框架会在表达式求值后将结果通过与replace()相同的路径写入输出区,因此两者行为完全一致。

1.5 输出格式化只发生一次

在 tests/_runtime/output/test_output.py 中,test_replace_formats_output_once验证了一个重要的实现细节:mo.output.replace(value)value的富展示格式化(如调用_repr_html_只执行一次,不会在渲染过程中重复触发格式化方法,这既保证了性能,也避免有状态对象的格式化被重复调用而产生副作用。

二、在 App 视图中显示单元格代码:mo.show_code()

默认情况下,以 App 形态运行 Notebook 时代码是隐藏的。若希望某个单元格的代码也出现在输出区、从而在 App 视图中可见,可以使用mo.show_code()

其函数签名与参数如下(marimo/_output/show_code.py):

mo.show_code(output: object = None, *, position: Literal["above", "below"] = "below") -> Html
  • output:要与代码一同展示的输出对象;省略时仅显示该单元格的代码本身
  • position:代码相对输出的位置,取值为"above"(代码在输出上方)或"below"(默认,代码在输出下方)。

实现上,show_code()会获取当前单元格的源代码,并通过正则替换把代码中的mo.show_code(...)调用替换为...(避免代码显示自身的递归循环,见 marimo/_output/show_code.py),然后借助只读的code_editor组件以代码高亮形式渲染,并与输出通过vstack垂直排列。

典型用法:

import marimo as mo def factorial(n: int) -> int: if n == 0: return 1 return n * factorial(n - 1) # 输出计算结果,并在其下方显示本单元格代码 mo.show_code(factorial(5)) # 只显示代码、不显示任何输出 # mo.show_code()

这一特性非常适合制作教学型、演示型 Notebook——读者在 App 视图中既能直接看到结果,也能看到产生结果的代码。

三、Console Outputs:stdout/stderr输出及其在 App 中的呈现

3.1 控制台输出的默认行为

写入stdout/stderr的文本——包括print语句和日志——会出现在单元格下方的控制台输出区域,与可视化输出区相互独立。例如:

print("这条文本出现在控制台输出区") "这是一个可视化输出"

需要特别注意的是:默认情况下,控制台输出在 App 视图中不显示。如果你希望print的输出出现在 App 界面中,就必须使用 marimo 提供的工具函数,将控制台输出捕获重定向为单元格输出。

3.2 捕获控制台输出:mo.capture_stdout()/mo.capture_stderr()

这两个上下文管理器将stdout/stderr写入捕获到内存缓冲区io.StringIO),并返回该缓冲区供你读取(marimo/_runtime/capture.py):

import marimo as mo with mo.capture_stdout() as buffer: print("Hello, world") mo.md(buffer.getvalue())

上面的示例正是 examples/outputs/capture_console_outputs.py 中的真实用法:先把print的输出捕获到buffer,再用mo.md(buffer.getvalue())将其作为可视化输出渲染到输出区——这样就实现了"让控制台文本出现在 App 中"。

对应的stderr捕获:

import marimo as mo import sys with mo.capture_stderr() as buffer: sys.stderr.write("Hello!") mo.md(buffer.getvalue())

底层实现中,marimo 的运行环境会将sys.stdout/sys.stderr替换为线程本地流代理(ThreadLocalStreamProxy)。capture_*临时把当前线程的流切换为StringIO,从而只捕获本线程的写入、不影响其他线程;在非 marimo 环境(代理不存在)下则回退到标准库的contextlib.redirect_stdout/redirect_stderr(marimo/_runtime/capture.py)。

3.3 重定向控制台输出:mo.redirect_stdout()/mo.redirect_stderr()

与"捕获后手动取出"不同,redirect_stdout/redirect_stderr将流写入直接转发到单元格的可视化输出区(marimo/_runtime/capture.py):

import marimo as mo with mo.redirect_stdout(): print("Hello!") print("World!") # 上面的 print 输出会直接出现在该单元格的输出区

这两个上下文管理器内部使用了一个_RedirectStream包装流:其write()方法把收到的文本通过_output.append(plain_text(msg))逐条追加到单元格输出(marimo/_runtime/capture.py)。也就是说,redirect_stdout是"实时流式"的——每条写入立即变成一条输出;而capture_stdout则是"整体捕获"——所有文本汇总到缓冲区后由你自行决定如何展示。二者取舍如下:

函数行为适用场景
mo.capture_stdout()/mo.capture_stderr()捕获到StringIO缓冲区,返回给用户需要对文本做二次处理(拼接、格式化、过滤)后再展示
mo.redirect_stdout()/mo.redirect_stderr()写入即时转发为单元格输出希望print的每一行直接出现在输出区,无需手动搬运

提示:重定向与捕获都发生在 marimo 运行时的线程局部流之上,多线程场景下只影响当前线程的流,这一点与 tests/_runtime/test_capture.py 中test_capture_both等用例验证的捕获行为一致。

3.4 清空控制台输出:mo.output.clear_console()

mo.output.clear_console()用于清空单元格下方的控制台输出区,包括同一次运行中更早写入的文本(来自print、日志或stdout/stderr)。从源码看,它执行两步操作(marimo/_runtime/output/_output.py):

  1. 调用ctx.stream.flush_console()刷新控制台流;
  2. 广播一个console=[]的空控制台通知,通知前端清空展示区域。

该行为在 tests/_runtime/output/test_output.py 的test_clear_console中有完整验证:单元格先print("secret")再调用clear_console(),测试断言前端收到了一条console == []status is None的清空通知。

import marimo as mo print("这条日志会被清掉") mo.output.clear_console() print("这条日志保留")

四、富对象检视:mo.inspect()

对于没有富展示形式_repr_html_等)的 Python 对象,marimo 提供了mo.inspect(obj),以丰富的 HTML 格式展示对象的属性、方法与文档,便于在 Notebook 中交互式探索 API(如argparse.ArgumentParser、SQLAlchemy 引擎等复杂对象)。其实现位于 marimo/_plugins/stateless/inspect.py,参数如下:

参数默认值作用
obj必填要检视的对象
helpFalse是否显示完整帮助文本(否则只显示第一段)
methodsFalse是否显示方法
docsTrue是否显示属性/方法的文档
privateFalse是否显示私有属性(以_开头)
dunderFalse是否显示双下划线特殊属性(以__开头)
sortTrue是否按字母序排列属性
allFalse一键开启methodsprivatedunder
valueTrue是否显示对象的值/repr

用法示例:

import marimo as mo import argparse # 检视一个没有富展示的对象,展示其方法 mo.inspect(argparse.ArgumentParser, methods=True) # 全量检视:方法 + 私有属性 + 特殊属性 mo.inspect(obj, all=True)

inspect本质上是Html的子类,会为不同类型(class/function/method/module/instance/object)渲染不同配色的类型徽标,并按参数控制展示的属性范围,让对象结构一目了然。

五、总结:输出控制的完整决策路径

围绕输出区,marimo 的 API 形成了一个完整的能力矩阵,可按下述思路选择:

  1. 默认行为:单元格最后一个表达式即为可视化输出,编辑时显示在代码上方,App 运行时即界面本身;
  2. 需要多段/动态输出:用mo.output.append()垂直堆叠,用mo.output.replace()覆盖旧输出,用mo.output.replace_at_index()定点更新;
  3. 注意替换陷阱:末尾的非None表达式会清空此前所有append内容,务必用mo.output.append(...)包裹最后一个表达式;
  4. 需要在 App 中显示代码:用mo.show_code(output, position="above"/"below")
  5. 需要把print/日志带进 App:用mo.redirect_stdout()/redirect_stderr()实时转发,或用mo.capture_stdout()/capture_stderr()捕获后自行加工,用mo.output.clear_console()控制台清屏;
  6. 需要检视复杂对象:用mo.inspect(obj, methods=True, ...)生成富 HTML 展示。

所有 API 均已通过marimo顶层命名空间导出(见 marimo/init.py),可直接以mo.output.replacemo.show_codemo.capture_stdout等形式调用。理解"可视化输出"与"控制台输出"这两条独立通道,以及replace/append的替换与堆叠语义,是编写界面友好、可分享、可部署的 marimo Notebook 的关键。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询