AI 技术博客
返回首页
Python 基础 · 17 分钟阅读

Jupyter Notebook 入门:数据探索的交互式工作台

## 引言 这是第一章《环境搭建与运行原理》的最后一篇。前 9 篇我们构建了一条完整的工具链:解释器(第 2 篇)、隔离环境(第 4 篇)、包管理(第 5 篇)、REPL(第 6 篇)、规范(第 7 篇)、调试(第 8 篇)、IDE(第 9 篇)。它们的共同点是"脚本 + 命令行"的形态:代码写成文件、整体运行、输出堆在终端里。但有一类工作用这种形态很别扭——**数据探索**:你面对一批数据,需要反复"换个角度看看",每个角度都是一段临时代码;你还要把"为什么这样分析、得出了什么"记录在旁边,供日后复用或向他人展示。 本文介绍的 **Jupyter Notebook** 正是为这类工作而生的交互式工作台:代码、运行结果、图、叙述性文字,全部组织在一份文档里,而且每段代码都能独立、反复地运行。读完本文,你会理解 notebook 的文档结构与前后端架构,掌握单元格操作、快捷键与魔法命令,并完整走一遍"提出问题 → 写代码 → 看输出 → 写结论"的数据探索闭环。读者应先掌握第 6 篇 REPL 的交互思维——Jupyter 的"一个格子一个格子地执行"正是 REPL 的文档化升级版。 ## 出身与原理:IPython 血统、内核架构与 .ipynb 文档 **Jupyter** 项目有一个清晰的传承:它的内核 **IPython**(Interactive Python 的缩写)由 Fernando Pérez 于 2001 年启动,是那个年代最强悍的 Python 交互环境;2014 年,IPython 团队把"交互式 Notebook"能力从语言绑定中剥离,成立了独立的 Jupyter 项目——名字取自三类最初支持语言的缩写:**Ju**lia、**Py**thon、**R**,寓意"面向数据科学的通用交互平台"。这项工作的代表性论文 *Jupyter Notebooks – a publishing format for reproducible computational workflows*(Kluyver 等,2015 年发表)至今仍是引用 Jupyter 时的标准出处。 理解 notebook 需要抓住三层结构: **第一层:文档格式。** 一个 notebook 文件的后缀是 `.ipynb`(interactive python notebook),本质是一份 **JSON 文本**。它由一组 **单元格(cell)** 组成,每个单元格都带有 `cell_type`(代码 `code` / 叙述 `markdown` / 原始 `raw`)、源代码 `source`、执行输出 `outputs` 与执行计数 `execution_count` 等字段。正因它是 JSON,notebook 才能被版本管理、被其他程序解析、被 nbconvert 转换为脚本或网页。我们稍后会用真实工具生成并检查一份 `.ipynb`。 **第二层:运行架构。** notebook 采用"**前端-内核**"分离架构:浏览器中的界面只是客户端,真正执行代码的是后台的**内核(kernel)**进程——对 Python 而言就是 IPython 内核(ipykernel)。前端把代码通过消息协议(底层是 ZeroMQ 与 websocket)发给内核,内核执行后把输出原路送回。这个架构有两个直接推论:其一,**不同内核间前端界面一模一样**,换一种语言只是换内核;其二,**内核是有状态的长驻进程**——`x = 1` 在单元格 1 执行后,单元格 2 再引用 `x` 完全正常,变量、导入、函数定义跨单元格存活,这正是 2000 年 IPython 交互式方言的延续。 **第三层:交互语义。** 与第 6 篇 REPL 一样,notebook 的每个代码单元格输入后即刻可运行(快捷键运行),输出紧跟单元格下方显示;不同的是输出会被**持久化进文档**,形成"代码-输出-叙述"交错的记录。这个思想可追溯到计算机科学家 Donald Knuth 1984 年提出的 **文学编程(literate programming)**:程序面向读者叙事,而非只面向机器。notebook 是文学编程在数据科学领域最成功的落地形态。 ## 安装与启动:一行命令开箱 安装与启动都极简(建议在前面创建的虚拟环境里执行): ```bash pip install jupyterlab jupyter lab ``` `jupyterlab`(JupyterLab)是 Jupyter 的新一代前端,比经典 `jupyter notebook` 功能更全(多标签、文件浏览器、终端)。执行 `jupyter lab` 后,终端会打印一屏启动日志,浏览器自动打开工作台主页;日志里那行 `http://localhost:8888/lab` 是本地服务地址——**别关终端窗口**,关闭它服务即停。想用经典界面则执行 `jupyter notebook`,二者共用同一套 `.ipynb` 文件。在主页左侧的文件浏览器里新建 Notebook(Python 3 内核),一个空 notebook 就绪。 ## 单元格操作:编辑模式、命令模式与快捷键 notebook 的每个单元格有两种模式:**编辑模式**(可输入代码/文字,单元格边框为绿色)与**命令模式**(对整个单元格操作,边框为蓝色)。用 Enter 进入编辑模式、Esc 返回命令模式。所有高频操作都有快捷键,以下一组是 Jupyter 官方文档中确定存在的,值得先背熟: - **Shift+Enter**:运行当前单元格并选中下一个(最常用); - **Ctrl+Enter**:运行当前单元格但不移动; - **Alt+Enter**:运行当前单元格并在下方新建一个; - 命令模式下:**A** 在上方插入单元格,**B** 在下方插入; - 命令模式下:**dd** 删除当前单元格; - 命令模式下:**m** 把单元格切换为 Markdown(叙述),**y** 切回 Code(代码); - 命令模式下:**H** 呼出完整的快捷键帮助面板。 单元格类型切换后别慌:Markdown 单元格里写的是**渲染后的叙述文字**,不是代码;按 m 的瞬间"代码变成了文字"是预期行为,Shift+Enter 运行它就会渲染出排版效果——标题用 `#`、列表用 `-`、行内代码用反引号、**加粗**与*斜体*两对标记。Markdown 还支持用美元符号包裹的 LaTeX 数学公式,例如 `$E = mc^2$`、`$\bar{x} = \frac{1}{n}\sum_{i=1}^{n} x_i$` 会被 MathJax 渲染成排版公式——写数据分析结论时,这是给变量"上户口"的标准姿势。 ## 魔法命令:notebook 里的增强指令 IPython 内核给交互环境增加了一类内置指令,叫**魔法命令(magic command)**,以 `%` 开头的是**行魔法**(作用于本行),以 `%%` 开头的是**单元魔法**(作用于整个单元格)。它们是 IPython 的扩展能力,不属于 Python 语法——这一点在易错点里会再强调。以下四个是数据探索里最高频的,全部真实可用(示例输出为本文作者在 3.12 + IPython 9 上实际运行的结果): **`%timeit 语句`**:反复执行一段代码,给出平均耗时与标准差,是性能直觉的来源: ```python %timeit sum(range(100)) ``` 真实输出(数值随机器而异,量级稳定): ```text 590 ns ± 13.4 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each) ``` **`%%timeit`**:同一命令的单元版本,用作测试整个代码块: ```python %%timeit total = sum(range(1000)) ``` 真实输出:`10.1 μs ± 280 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)`——注意 `%%timeit` 会重复执行**整个单元格**,所以单元格里不要放有副作用的代码(例如写文件)。 **`!命令`**:把整行作为系统 shell 命令执行(Windows 上走 PowerShell/cmd,Linux/macOS 走 bash): ```python !echo hello-from-notebook ``` 输出 `hello-from-notebook`。这对"做完分析顺手看一眼目录里有什么"极其顺手:`!dir`(Windows)或 `!ls -l`(Unix 风格)即可。 **`%pwd` 与 `%who`**:前者显示内核当前工作目录,后者列出当前命名空间里你自定义的变量名。这两个都是"你在哪、你有什么"的导航类命令,用于防止在长会话里迷失。 ```python %who ``` 真实输出:`x y `——列出了本会话定义过的变量名(空格与制表符分隔)。此外还有 `%load 文件`(把文件内容读进单元格)与 `%run 脚本`(把脚本当作模块执行并保留其变量),第 9 篇提到过 VS Code 让代码流向 notebook,`%load` 则是反向流动:把已验证的 `.py` 脚本捞回 notebook 继续加工。 ## 实战闭环:一次真实的成绩数据分析 现在把以上全部组装进一个小案例,它对应的 notebook 由四个单元格组成。第一步,生成模拟数据(用随机数与标准库统计模块,不依赖第三方库): ```python import random random.seed(42) scores = [random.randint(40, 100) for _ in range(30)] ``` 第二步,计算集中趋势与离散程度——注意 `statistics` 是标准库,直接可用: ```python from statistics import mean, median, pstdev mean(scores), median(scores), pstdev(scores) ``` 第三步,写结论(Markdown 单元格): ```markdown # 成绩概览 这 30 个模拟成绩中,平均值约为 64.5 分,中位数为 62 分, 中位数略低于平均值,说明分布右偏(高分段尾巴更长); 总体标准差约 17.6 分,离散程度中等。 用公式表达:$\bar{x} \approx 64.5$,$s \approx 17.6$。 ``` (以上数值来自 `random.seed(42)` 固定种子下的真实运行结果;想验证"种子决定数据",把 seed 那一行删掉重跑整个文档,全部数值都会变。) 三个单元格运行下来,你会亲身体会 notebook 与脚本的核心差异:**每个单元格都是快照可单独重跑**——改一行、Shift+Enter、输出即时更新,叙述文字跟着改,整个过程不需要重跑其他单元格。这就是数据探索工作流的形态:快的反馈循环 + 活的文档。 再看一眼文档本质,验证"notebook 是 JSON"的说法。上面这个 notebook 保存后,用 Python 重新读入并检查结构(`nbformat` 是官方库,`nbclient` 能让代码在没有浏览器的情况下执行 notebook——它们也是我们验证本例输出真实的工具): ```python import nbformat nb = nbformat.read("scores_demo.ipynb", as_version=4) print([cell.cell_type for cell in nb.cells]) ``` 输出 `['code', 'code', 'markdown']`,与手工构建的文档应和一致。在这个结构上还能做一件大事:**无浏览器执行**。用 `nbclient` 的 `NotebookClient` 对一个 notebook 调用 `execute()`,等价于"从头到尾按顺序运行一遍所有单元格"——这是可复现分析(reproducibility)的基石:任何拿到 `.ipynb` 的人,都能确定性地重现你的结果,而不是依赖你随手敲出的顺序。 ## 分享与版本控制:nbconvert 与 diff 噪音 分析做完要交付,`.ipynb` 虽好,但并非所有场合都适合直接分发。**nbconvert** 是官方转换器,可把 notebook 导出为多种格式: ```bash jupyter nbconvert --to script scores_demo.ipynb jupyter nbconvert --to html scores_demo.ipynb ``` `--to script` 把每个单元格提取成一段 Python 脚本(单元格间用 `# In[1]:` 注释隔开),适合"把研究成果转成正式代码";`--to html` 则是保留输出的静态网页,适合给不看代码的人。这两个命令真实可运行(本文验证时生成了 126 字节的脚本文件,内容与单元格一一对应)。 版本控制是另一个常见需求。`.ipynb` 是 JSON,每次运行都会更新 `outputs` 字段——于是提交一次代码改动,diff 里却塞满了输出内容的变化,这正是"notebook + Git 体验差"的根源。工程对策有两个:提交前用 `jupyter nbconvert --to notebook --clear-output --inplace scores_demo.ipynb` 清空输出(`--clear-output` 清空、`--inplace` 原地保存,都是真实选项);或者对比工具直接用社区标准 nbdime(专门面向 notebook 的 diff/merge 工具)。记住原则:**输出是缓存,代码是真相**——入库的是代码,输出留给本地。 ## 易错点与陷阱 **1. 执行顺序 ≠ 视觉顺序。** notebook 允许任意单元格在任何时刻被运行,于是一个"看起来自上而下"的文档,实际执行可能是 3→1→2。后果很隐蔽:单元格 3 依赖单元格 1 的变量,但单元格 2 又改了那个变量——程序不报错,结果却对不上视觉顺序。这是 notebook 最经典的研究可信度陷阱(不少论文复现事故源于此)。对策:分析告一段落,用菜单里的 **"Restart Kernel and Run All Cells…"** 从头到尾重跑一遍,能一次跑通才说明文档是自洽的;这是 JupyterLab 的真实菜单项,值得养成肌肉记忆。 **2. 把魔法命令当 Python 语法。** `%timeit`、`%%timeit`、`!` 全部属于 IPython 内核的扩展,不是 Python 语言的一部分。把 `%%timeit` 原样复制进 `.py` 文件运行,立刻语法错误(`%` 在 Python 里是取模运算符,`%%timeit` 这种写法无法解析)——本文验证时 `python magic_bad.py` 直接报 `SyntaxError`。反过来,`.py` 文件里能写的东西 notebook 里都能写,但**魔法命令只能活在 notebook(或 IPython 交互环境)里**,交付正式代码时务必把它们替换为 `timeit` 模块等价的纯 Python 写法。 **3. 关闭浏览器以为程序停了,内核却还在跑。** 前端-内核架构的另一面:浏览器标签页关掉,**内核进程不一定退出**(长时间运算可能仍在后台继续),服务器列表里能看到这些"孤儿内核"。反之,重启内核(Kernel 菜单)会清空**所有**变量、导入与函数定义,之前依赖的"上次运行的隐式状态"全部失效。规矩:需要干净环境就重启内核;用完一次性的重型计算,主动用 Kernel 菜单关闭对应内核,别让机器背着无主进程。 **4. 依赖全局变量而非显式参数。** 因为变量跨单元格存活,很多分析代码养成了"直读上一个单元格的全局量"的写法。这在 notebook 里一时无碍,但同样的代码搬进 `.py` 或换台机器跑,立刻崩。工程化的好习惯:把关键分析封装成函数,参数显式传入;单元格里只放"读取数据 → 调用函数 → 展示结果"的薄壳。这与第 4 章函数与模块化的道理一脉相承,notebook 恰恰是最容易滑向"面条式全局量"的环境。 ## 小结 Jupyter Notebook 是面向数据探索的交互式工作台:`.ipynb` 本质是 JSON 文档,按单元格组织代码、输出与叙述(文学编程思想的落地);运行遵循"前端-内核"架构,内核是长驻的 IPython 进程,变量跨单元格存活。本文覆盖了它的完整使用面:安装启动(`pip install jupyterlab` + `jupyter lab`)、单元格操作与快捷键(Shift+Enter 运行、A/B 增格、dd 删除、m/y 切换类型)、Markdown 与 MathJax 数学、魔法命令(%timeit、%%timeit、!、%pwd、%who),并用一个成绩分析的完整闭环演示了数据探索工作流;最后讨论了 nbconvert 导出、--clear-output 清缓存与四个高频陷阱(执行顺序≠视觉顺序、魔法命令出不了 notebook、孤儿内核、全局变量依赖)。至此,第一章的环境与工具链全部就绪:第 2 篇到第 10 篇,从装解释器到分析数据,你已经具备继续学习第二章"变量、类型与运算符"的全部前置条件。 ## 练习与思考题 1. 用 `pip install jupyterlab` 启动 notebook,把本文"成绩数据分析"的四个单元格完整重做一遍,并回答:为什么 `random.seed(42)` 这个单元格必须**在**生成数据的单元格之前运行?(答案导向:seed 确定随机序列;顺序颠倒则数据不同,且"Restart and Run All"会暴露依赖顺序。) 2. 在 notebook 中分别执行 `%timeit` 与 `%%timeit` 测同一段代码,两者耗时数量级有何差别?结合单元格重复执行机制解释原因。(答案导向:`%%timeit` 把整个单元格放入计时循环重复执行;若单元格内有副作用则结果失真。) 3. 用 `nbformat` 读取你刚才保存的 notebook,打印第一个代码单元格的 `source` 与 `outputs` 字段,观察"无执行计数"与"有执行计数"的差异,说出 `execution_count` 为 `None` 意味着什么。(答案导向:从未执行过该单元格。) 4. 综合应用题:把成绩分析写成"函数 + 数据读取 + 展示"的三段式单元格,然后用 `jupyter nbconvert --to script` 导出脚本并直接 `python` 运行,体会 notebook 内运行与脚本运行的差异,并总结:什么情况下你会选择 notebook、什么情况下选择脚本?(答案导向:探索与叙述用 notebook,交付与自动化用脚本。)