Python 与 Jupyter:交互式计算、数据分析与可复现研究
系统阐述 Jupyter Notebook/Lab 的架构、内核协议、魔法命令、可视化、Widgets、性能优化与企业级部署实践。
Python 与 Jupyter:交互式计算、数据分析与可复现研究
本文系统阐述 Jupyter 生态的核心机制与工程实践,包括 IPython 内核、Jupyter Message Protocol、Notebook 文档格式(.ipynb)、魔法命令系统、富显示对象、交互式 Widgets、可视化集成、性能优化(Cython、numba、joblib)、企业级部署(JupyterHub、Enterprise Gateway)与可复现研究实践。内容兼顾形式化定义与生产级应用,旨在帮助开发者建立对交互式计算的完整认知框架,具备构建数据科学工作流与生产级 Jupyter 平台的能力。
1. 学习目标
本文依据 Bloom’s Taxonomy(布鲁姆认知目标分类学)的六个层次组织学习目标,确保从低阶认知到高阶创造的渐进式掌握。
1.1 记忆(Remembering)
- 列出 Jupyter 项目的五大组成:Notebook、Lab、Hub、Enterprise Gateway、Voila。
- 回忆 IPython 内核的五大魔法命令分类:行魔法、单元格魔法、Shell 魔法、调试魔法、配置魔法。
- 列出 Notebook 文档(.ipynb)的 JSON 结构:
cells、metadata、nbformat、nbformat_minor。 - 陈述 Jupyter Message Protocol 的消息类型:
execute_request、execute_result、display_data、stream、error。
1.2 理解(Understanding)
- 解释 Jupyter 前端(Notebook/Lab)与后端(Kernel)的解耦架构。
- 描述消息协议中 ZeroMQ 的四个套接字:Shell、IOPub、Stdin、Control。
- 区分行魔法(
%magic)与单元格魔法(%%magic)的作用范围与语法。 - 解释 Kernel 的执行模型:单线程事件循环、REPL 语义、命名空间持久化。
1.3 应用(Applying)
- 使用
%%timeit、%prun、%lprun进行性能剖析。 - 通过
IPython.display模块展示 HTML、Markdown、Image、JSON 等富对象。 - 使用
ipywidgets构建交互式控件(滑块、下拉、按钮)。 - 编写自定义魔法命令扩展 IPython 功能。
1.4 分析(Analyzing)
- 分析 Notebook 在版本控制中的挑战(输出嵌入、执行顺序)与解决方案(nbstripout、jupytext)。
- 解构 Kernel 重启对状态的影响:变量丢失、导入丢失、连接保持。
- 比较不同可视化库(Matplotlib、Plotly、Bokeh、Altair)在 Jupyter 中的集成方式与适用场景。
- 分析 Notebook 的”隐式状态”问题:单元格执行顺序导致的可复现性陷阱。
1.5 评价(Evaluating)
- 评估 Jupyter Notebook 在生产环境(模型训练、报表生成)中的适用性。
- 评判 nbconvert 与 papermill 在批量执行 Notebook 时的优劣。
- 评价 JupyterHub 多租户方案在团队数据科学平台中的设计合理性。
1.6 创造(Creating)
- 设计一套基于 papermill + Airflow 的可复现数据分析流水线。
- 构建自定义 Jupyter Kernel(如 Bash Kernel、SQL Kernel)。
- 实现一个基于 Voila 的交互式数据仪表板。
2. 历史动机与背景
2.1 IPython 的诞生
2001 年,Fernando Pérez 在科罗拉多大学攻读物理学博士时,为了方便科学计算,开发了 IPython(Interactive Python)。IPython 最初是对 Python 默认 REPL 的增强,提供了:
- 更友好的交互式提示符(
In [1]:/Out [1]:) - 魔法命令系统(
%run、%timeit、%pwd) - 对象内省(
obj?查看文档,obj??查看源码) - Shell 命令集成(
!ls、!pwd) - 调试器集成(
%debug、%pdb)
2.2 从 IPython 到 Jupyter
2014 年,IPython 项目拆分为两部分:
- IPython:保留 Python 交互式 shell 的核心功能。
- Jupyter:通用 Notebook 架构,支持任意编程语言的内核。
Jupyter 名字来源于三大初始语言:Julia、Python、R。拆分的动机是:
- 架构解耦:Notebook 前端与语言后端分离,支持 Julia、R、Haskell、Scala 等 40+ 语言内核。
- 治理独立:Jupyter 成为独立项目,由 NumFOCUS 基金会管理。
- 生态扩展:衍生 JupyterLab(IDE 式界面)、JupyterHub(多用户平台)、Voila(仪表板)等。
2.3 JupyterLab 的演进
2018 年发布 JupyterLab 1.0,作为 Notebook 的下一代界面:
- 多窗格布局:支持并排编辑、拖拽布局。
- 文件浏览器:集成文件管理。
- 集成 IDE 特性:代码补全、跳转定义、变量浏览器、调试器。
- 扩展系统:基于 npm 的插件机制(JupyterLab 3.x 改为预编译扩展)。
2023 年的 JupyterLab 4.x 进一步优化性能(CodeMirror 6)、增强调试器、改进 RTL 支持。
2.4 企业级应用
Jupyter 从科研工具演变为企业数据科学平台的核心组件:
- JupyterHub:多用户平台,支持 LDAP/OAuth 认证、Kubernetes 部署。
- Enterprise Gateway:在远程集群(Kubernetes、YARN、Condor)中启动 Kernel。
- Voila:将 Notebook 转换为交互式 Web 应用。
- Jupyter Enterprise:云厂商提供托管服务(AWS SageMaker、GCP Vertex AI、Azure ML)。
3. 形式化定义
3.1 Jupyter 架构形式化
Jupyter 可形式化为四元组:
- :前端(Frontend),Notebook 或 JupyterLab 的浏览器界面
- :内核(Kernel),执行代码的进程,可形式化为
- :编程语言(Python、R、Julia)
- :命名空间(Name Space),变量与导入的集合
- :执行函数,
- :协议(Protocol),Jupyter Message Protocol(基于 ZeroMQ + WebSocket)
- :消息集合,
3.2 Notebook 文档形式化
Notebook 文档 是 JSON 对象:
其中 是单元格列表:
- :源代码或 Markdown 文本(字符串列表)
- :执行输出(仅 code 单元格)
- :执行序号
3.3 消息协议形式化
Jupyter 消息 可形式化为五元组:
- :消息体,结构随 变化
3.4 执行模型形式化
Kernel 的执行模型可形式化为状态机:
- :时刻 的命名空间状态
- :第 个单元格的代码
- :执行函数,更新命名空间并产生输出
关键性质:命名空间持久化。同一 Kernel 会话内,所有单元格共享 ,后执行单元格可访问前序单元格的变量。这是 Notebook “状态ful”特性的根源。
3.5 魔法命令形式化
魔法命令可形式化为函数映射:
- 行魔法(Line Magic):,作用于单行。
- 单元格魔法(Cell Magic):,作用于整个单元格。
4. 理论推导
4.1 Jupyter Message Protocol
4.1.1 ZeroMQ 套接字模型
Jupyter Kernel 与前端通过 ZeroMQ 的四个套接字通信:
+-----------+ +-----------+
| Frontend | | Kernel |
| (Browser)| | (Process) |
+-----------+ +-----------+
| |
| Shell (ROUTER/DEALER) - 请求-响应:execute_request, execute_reply
| |
| IOPub (PUB/SUB) - 广播:execute_result, stream, display_data
| |
| Stdin (ROUTER/DEALER) - 输入请求:input_request, input_reply
| |
| Control (ROUTER/DEALER) - 控制:interrupt_request, shutdown_request
v v
- Shell 套接字:处理代码执行请求,单线程顺序处理。
- IOPub 套接字:广播执行产生的输出(stdout、结果、错误)。
- Stdin 套接字:处理
input()调用,前端弹出输入框。 - Control 套接字:处理中断、关闭等控制命令,独立线程。
4.1.2 执行流程
用户执行单元格的完整流程:
- 前端通过 Shell 套接字发送
execute_request,包含代码与execution_count。 - Kernel 接收后立即回复
execute_reply(状态busy),开始执行。 - Kernel 通过 IOPub 广播
status: busy,告知所有订阅者。 - 执行过程中:
print()输出 →stream消息- 表达式结果 →
execute_result消息 display()调用 →display_data消息- 异常 →
error消息(包含 traceback)
- 执行结束,Kernel 通过 IOPub 广播
status: idle。 - 通过 Shell 发送
execute_reply(状态ok或error)。
4.1.3 消息示例
# execute_request 消息结构(简化)
{
"header": {
"msg_id": "abc123",
"username": "user",
"session": "session456",
"msg_type": "execute_request",
"version": "5.3",
"date": "2026-07-21T10:00:00"
},
"parent_header": {},
"metadata": {},
"content": {
"code": "print('Hello')",
"silent": false,
"store_history": true,
"user_expressions": {},
"allow_stdin": true,
"stop_on_error": true
},
"buffers": []
}
# execute_result 消息
{
"header": {"msg_type": "execute_result", ...},
"parent_header": {"msg_id": "abc123", ...},
"content": {
"execution_count": 1,
"data": {"text/plain": "Hello"},
"metadata": {}
}
}
4.2 IPython 内核的执行引擎
4.2.1 输入转换
IPython 在执行代码前进行输入转换:
- 魔法命令解析:
%cmd与%%cmd转换为对应的 Python 函数调用。 - Shell 命令解析:
!cmd转换为getoutput(cmd)。 - 帮助语法:
obj?转换为pinfo(obj),obj??转换为pinfo2(obj)。 - 自动缩进:多行语句的自动续行。
# IPython 输入转换示例
# 用户输入:%timeit sum(range(100))
# 转换后:
get_ipython().run_line_magic('timeit', 'sum(range(100))')
# 用户输入:%%timeit
# sum(range(100))
# 转换后:
get_ipython().run_cell_magic('timeit', '', 'sum(range(100))')
4.2.2 显示钩子
IPython 重写 sys.displayhook,使表达式最后一行的值自动显示:
# IPython 的 displayhook 逻辑(简化)
def displayhook(value):
if value is None:
return
# 调用富显示协议
formatter = get_ipython().display_formatter
format_dict, metadata = formatter.format(value)
# 发送 execute_result 消息
send_execute_result(format_dict, metadata)
4.2.3 富显示协议
任何对象可通过 _repr_*_() 方法定义多种格式的显示:
class RichObject:
"""富显示对象示例"""
def __init__(self, data):
self.data = data
def _repr_html_(self):
"""HTML 格式显示"""
return f"<table><tr><td>{self.data}</td></tr></table>"
def _repr_json_(self):
"""JSON 格式显示"""
return {"data": self.data}
def _repr_pretty_(self, pp, cycle):
"""纯文本格式显示"""
pp.text(f"RichObject({self.data})")
def _repr_svg_(self):
"""SVG 格式显示"""
return f'<svg><text>{self.data}</text></svg>'
def _repr_png_(self):
"""PNG 格式显示(返回 bytes)"""
# 生成 PNG 图像
return b'\x89PNG...', {"width": 400, "height": 300}
前端根据优先级选择最适合的格式显示(JupyterLab 优先 HTML/SVG,纯文本回退)。
4.3 魔法命令系统
4.3.1 内置魔法命令
# 性能剖析
%timeit sum(range(1000)) # 多次执行测量平均耗时
%time sum(range(1000)) # 单次执行测量耗时
%prun -s cumulative func() # 性能剖析(按累计时间排序)
%lprun -f func func() # 行级剖析(需 line_profiler)
%memit big_list = list(range(10**7)) # 内存剖析(需 memory_profiler)
# Shell 集成
!ls -la # 执行 Shell 命令
files = !ls # 将 Shell 输出赋值给变量
%pwd # 当前工作目录
%cd /tmp # 切换目录
%env # 查看环境变量
# 调试
%debug # 进入调试器(事后调试)
%pdb # 切换异常自动进入调试器
%run script.py # 运行外部脚本
%load script.py # 加载脚本到单元格
# 代码分析
%who # 列出所有变量
%whos # 列出变量及类型与值
%reset # 重置命名空间
%xdel variable # 删除变量并尝试清除引用
# 历史
%history # 查看历史
%save script.py 1-10 # 保存历史到文件
%rerun # 重新执行历史
4.3.2 单元格魔法
# 性能剖析单元格
%%timeit
# 整个单元格多次执行
for i in range(1000):
pass
# 写入文件
%%writefile output.txt
这是要写入文件的内容
多行文本
# 执行其他语言
%%bash
echo "Hello from Bash"
ls -la
%%html
<table><tr><td>HTML 内容</td></tr></table>
%%javascript
console.log("Hello from JS");
%%latex
\begin{equation}
E = mc^2
\end{equation}
# 性能剖析
%%cython
# 编译为 C 扩展
def fib(int n):
cdef int i
cdef double a = 0, b = 1
for i in range(n):
a, b = b, a + b
return a
4.3.3 自定义魔法命令
from IPython.core.magic import Magics, magics_class, line_magic, cell_magic
from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
@magics_class
class MyMagics(Magics):
"""自定义魔法命令集合"""
@line_magic
@magic_arguments()
@argument('text', help='要打印的文本')
@argument('--upper', action='store_true', help='是否大写')
def greet(self, line):
"""%greet - 自定义问候命令"""
args = parse_argstring(self.greet, line)
text = args.text
if args.upper:
text = text.upper()
print(f"Hello, {text}!")
@cell_magic
def to_upper(self, line, cell):
"""%%to_upper - 将单元格内容转为大写"""
print(cell.upper())
# 注册魔法命令
def load_ipython_extension(ipython):
"""加载 IPython 扩展"""
ipython.register_magics(MyMagics)
# 在 Notebook 中使用:%load_ext mymodule
# 然后调用:%greet world --upper
4.4 ipywidgets 交互式控件
4.4.1 核心控件
import ipywidgets as widgets
from IPython.display import display
# 滑块
slider = widgets.IntSlider(
value=50,
min=0,
max=100,
step=1,
description='值:',
continuous_update=False
)
display(slider)
# 下拉框
dropdown = widgets.Dropdown(
options=['选项1', '选项2', '选项3'],
value='选项1',
description='选择:'
)
display(dropdown)
# 文本框
text = widgets.Text(
value='',
placeholder='输入文本',
description='文本:'
)
display(text)
# 按钮
button = widgets.Button(description="点击我")
output = widgets.Output()
def on_button_click(b):
with output:
print("按钮被点击!")
button.on_click(on_button_click)
display(button, output)
4.4.2 interact 装饰器
from ipywidgets import interact
@interact(x=(0, 10, 1), y=['a', 'b', 'c'])
def func(x=5, y='a'):
"""自动根据参数生成控件"""
print(f"x={x}, y={y}")
# 等价于手动创建控件并绑定
@interact
def func(x=5, y='a'):
print(f"x={x}, y={y}")
4.4.3 复杂布局
# 水平布局
hbox = widgets.HBox([slider, dropdown, button])
display(hbox)
# 垂直布局
vbox = widgets.VBox([slider, dropdown, button])
display(vbox)
# 标签页
tab = widgets.Tab()
tab.children = [widgets.HTML('<h2>第一页</h2>'),
widgets.HTML('<h2>第二页</h2>')]
tab.titles = ['页面1', '页面2']
display(tab)
# 网格布局
grid = widgets.GridBox(
[widgets.Button(description=f'按钮{i}') for i in range(6)],
layout=widgets.Layout(grid_template_columns='repeat(3, 1fr)')
)
display(grid)
4.5 可视化集成
4.5.1 Matplotlib
import matplotlib.pyplot as plt
import numpy as np
# 内联显示(默认)
%matplotlib inline
# 交互式显示(弹窗)
# %matplotlib qt
# Notebook 内交互(可缩放)
# %matplotlib notebook
# 绘图
x = np.linspace(0, 10, 100)
y = np.sin(x)
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, y, label='sin(x)')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('正弦曲线')
ax.legend()
ax.grid(True)
plt.show()
4.5.2 Plotly 交互式可视化
import plotly.graph_objects as go
fig = go.Figure(data=go.Scatter(
x=x,
y=y,
mode='lines+markers',
name='sin(x)'
))
fig.update_layout(
title='交互式正弦曲线',
xaxis_title='x',
yaxis_title='y',
hovermode='x unified'
)
fig.show()
4.5.3 Altair 声明式可视化
import altair as alt
import pandas as pd
df = pd.DataFrame({'x': x, 'y': y})
chart = alt.Chart(df).mark_line().encode(
x='x',
y='y'
).properties(
title='声明式正弦曲线'
)
chart.show()
4.6 Notebook 文档格式
4.6.1 .ipynb 结构
{
"nbformat": 4,
"nbformat_minor": 5,
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.11.0",
"mimetype": "text/x-python",
"file_extension": ".py",
"codemirror_mode": {"name": "ipython", "version": 3}
}
},
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": ["# 标题\n", "这是 Markdown 单元格"]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"source": ["print('Hello')"],
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": ["Hello\n"]
}
]
}
]
}
4.6.2 nbformat 版本
- nbformat 4:当前主流版本,2014 年随 Jupyter 发布。
- nbformat 3:旧 IPython 格式,已被淘汰。
- nbformat_minor:小版本号,引入新特性(如
cell_id)。
5. 代码示例
5.1 IPython 基础:内省与帮助
"""
IPython 内省与帮助系统示例
演示 IPython 增强的交互式特性
"""
# 对象内省:obj? 查看文档,obj?? 查看源码
import numpy as np
# 查看函数文档
# np.array? # 在 Notebook 中输入此行
# 查看函数源码
# np.array?? # 在 Notebook 中输入此行
# 通配符搜索
# np.*arr*? # 列出所有包含 'arr' 的名称
# Shell 命令集成
files = !ls *.ipynb
print(f"当前目录的 Notebook 文件: {files}")
# 环境变量
user = !echo $USER
print(f"当前用户: {user}")
# 历史
# %history -n 1-5 # 查看前 5 条历史
5.2 性能剖析完整示例
"""
性能剖析完整示例
演示 %timeit、%prun、%lprun、%memit 的使用
"""
import numpy as np
import time
def slow_function(n):
"""故意低效的函数:使用循环计算平方和"""
result = 0
for i in range(n):
result += i ** 2
return result
def fast_function(n):
"""使用 NumPy 向量化"""
return np.sum(np.arange(n) ** 2)
# 对比性能
# %timeit slow_function(10000) # 约 5ms
# %timeit fast_function(10000) # 约 50us
# 性能剖析
# %prun -s cumulative slow_function(100000)
# 行级剖析(需先安装 line_profiler)
# %load_ext line_profiler
# %lprun -f slow_function slow_function(100000)
# 内存剖析(需先安装 memory_profiler)
# %load_ext memory_profiler
# %memit big = list(range(10**7))
5.3 富显示对象
"""
富显示协议示例
演示如何让自定义对象在 Notebook 中以多种格式显示
"""
from IPython.display import HTML, JSON, Markdown, Image, display
import pandas as pd
class DataFrameReporter:
"""数据框报告器:以多种格式展示 DataFrame"""
def __init__(self, df, title="数据报告"):
self.df = df
self.title = title
def _repr_html_(self):
"""HTML 格式(Notebook 默认)"""
return f"""
<div style="border: 1px solid #ccc; padding: 10px;">
<h3>{self.title}</h3>
{self.df.to_html(index=False)}
<p>共 {len(self.df)} 行</p>
</div>
"""
def _repr_json_(self):
"""JSON 格式"""
return {
"title": self.title,
"rows": len(self.df),
"columns": list(self.df.columns),
"data": self.df.to_dict(orient='records')
}
def _repr_pretty_(self, pp, cycle):
"""纯文本格式"""
pp.text(f"DataFrameReporter(title='{self.title}', rows={len(self.df)})")
# 使用
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie'],
'age': [25, 30, 35],
'score': [85.5, 92.0, 78.5]
})
reporter = DataFrameReporter(df, title="学生成绩表")
reporter # 自动调用 _repr_html_
# 显式显示特定格式
display(HTML(f"<h2>数据概览</h2>{df.describe().to_html()}"))
display(JSON({"summary": df.describe().to_dict()}))
display(Markdown(f"""
## 数据摘要
- 行数:{len(df)}
- 列数:{len(df.columns)}
- 平均年龄:{df['age'].mean():.1f}
"""))
5.4 交互式数据分析
"""
交互式数据分析示例
使用 ipywidgets 构建可交互的数据探索工具
"""
import ipywidgets as widgets
from IPython.display import display, clear_output
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
# 生成示例数据
np.random.seed(42)
data = pd.DataFrame({
'x': np.random.randn(1000),
'y': np.random.randn(1000),
'category': np.random.choice(['A', 'B', 'C'], 1000)
})
# 创建控件
category_filter = widgets.Dropdown(
options=['全部', 'A', 'B', 'C'],
value='全部',
description='类别:'
)
bin_count = widgets.IntSlider(
value=30,
min=5,
max=100,
step=5,
description=' bins:',
continuous_update=False
)
chart_type = widgets.ToggleButtons(
options=['histogram', 'scatter', 'box'],
description='图表:'
)
output = widgets.Output()
def update_chart(**kwargs):
"""更新图表"""
with output:
clear_output(wait=True)
# 筛选数据
cat = kwargs['category_filter']
if cat != '全部':
df = data[data['category'] == cat]
else:
df = data
# 绘图
fig, ax = plt.subplots(figsize=(8, 5))
if kwargs['chart_type'] == 'histogram':
ax.hist(df['x'], bins=kwargs['bin_count'], alpha=0.7, edgecolor='black')
ax.set_xlabel('x')
ax.set_ylabel('频数')
ax.set_title(f'{cat} 类别 - 直方图')
elif kwargs['chart_type'] == 'scatter':
scatter = ax.scatter(df['x'], df['y'], c=df['category'].astype('category').cat.codes, alpha=0.5)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title(f'{cat} 类别 - 散点图')
elif kwargs['chart_type'] == 'box':
df.boxplot(column='x', by='category', ax=ax)
ax.set_title('箱线图')
plt.tight_layout()
plt.show()
# 绑定交互
widgets.interactive(update_chart,
category_filter=category_filter,
bin_count=bin_count,
chart_type=chart_type)
5.5 自定义内核示例
"""
自定义 Jupyter Kernel 示例
演示如何创建一个简单的 Echo Kernel
"""
from jupyter_kernel import JupyterKernel
class EchoKernel(JupyterKernel):
"""回显内核:原样返回输入代码"""
implementation = 'Echo'
implementation_version = '1.0'
language = 'echo'
language_version = '1.0'
language_info = {
'name': 'echo',
'mimetype': 'text/plain',
'file_extension': '.txt',
}
banner = "Echo Kernel - 原样返回输入"
def do_execute(self, code, silent, store_history=True,
user_expressions=None, allow_stdin=False):
"""执行代码"""
if not silent:
# 发送输出
stream_content = {'name': 'stdout', 'text': code + '\n'}
self.send_response(self.iopub_socket, 'stream', stream_content)
return {
'status': 'ok',
'execution_count': self.execution_count,
'payload': [],
'user_expressions': {}
}
# 注册内核
# 在 kernelspec 目录创建 kernel.json:
# {
# "argv": ["python", "-m", "echo_kernel", "-f", "{connection_file}"],
# "display_name": "Echo",
# "language": "echo"
# }
5.6 nbconvert 批量转换
"""
nbconvert 示例:将 Notebook 转换为多种格式
"""
import subprocess
# 转换为 HTML
subprocess.run([
'jupyter', 'nbconvert',
'--to', 'html',
'--execute', # 执行所有单元格
'--output-dir', './output',
'analysis.ipynb'
])
# 转换为 PDF(需 LaTeX)
subprocess.run([
'jupyter', 'nbconvert',
'--to', 'pdf',
'--output-dir', './output',
'report.ipynb'
])
# 转换为 Python 脚本
subprocess.run([
'jupyter', 'nbconvert',
'--to', 'script',
'analysis.ipynb'
])
# 转换为 Markdown
subprocess.run([
'jupyter', 'nbconvert',
'--to', 'markdown',
'--output-dir', './docs',
'analysis.ipynb'
])
5.7 papermill 参数化执行
"""
papermill 示例:参数化批量执行 Notebook
"""
import papermill as pm
# 参数化执行单个 Notebook
pm.execute_notebook(
input_path='template.ipynb',
output_path='output_run1.ipynb',
parameters={
'alpha': 0.1,
'iterations': 1000,
'dataset': 'iris.csv'
}
)
# 批量执行:不同参数组合
parameter_sets = [
{'alpha': 0.01, 'iterations': 500},
{'alpha': 0.1, 'iterations': 1000},
{'alpha': 0.5, 'iterations': 2000},
]
for i, params in enumerate(parameter_sets):
pm.execute_notebook(
input_path='template.ipynb',
output_path=f'output_run_{i}.ipynb',
parameters=params
)
5.8 Voila 仪表板
"""
Voila 仪表板示例
将 Notebook 转换为交互式 Web 应用
"""
# 在 Notebook 中定义 UI,Voila 会隐藏代码、保留输出
import ipywidgets as widgets
from IPython.display import display
import matplotlib.pyplot as plt
import numpy as np
# 标题
title = widgets.HTML("<h1>股票价格模拟器</h1>")
display(title)
# 参数控件
initial_price = widgets.FloatSlider(value=100, min=50, max=200, description='初始价格:')
volatility = widgets.FloatSlider(value=0.2, min=0.05, max=0.5, step=0.01, description='波动率:')
days = widgets.IntSlider(value=30, min=5, max=365, description='天数:')
run_button = widgets.Button(description='模拟')
output = widgets.Output()
def simulate(b):
"""模拟股价"""
with output:
output.clear_output()
prices = [initial_price.value]
for _ in range(days.value):
ret = np.random.normal(0, volatility.value)
prices.append(prices[-1] * (1 + ret))
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(prices)
ax.set_xlabel('天数')
ax.set_ylabel('价格')
ax.set_title('股价模拟')
plt.show()
run_button.on_click(simulate)
# 布局
dashboard = widgets.VBox([
title,
widgets.HBox([initial_price, volatility, days]),
run_button,
output
])
display(dashboard)
# 启动 Voila(终端执行):
# voila dashboard.ipynb --port 8866
6. 对比分析
6.1 Jupyter 界面对比
| 特性 | Notebook 7 | JupyterLab 4 | VS Code Notebook |
|---|---|---|---|
| 界面风格 | 经典单列 | IDE 式多窗格 | 编辑器集成 |
| 多窗格 | 不支持 | 支持(拖拽) | 支持(标签页) |
| 调试器 | 基础 | 完整 | 强大 |
| 变量浏览器 | 扩展 | 内置 | 内置 |
| 扩展生态 | 有限 | 丰富 | VS Code 插件 |
| 文件管理 | 简单 | 完整 | 完整 |
| 性能 | 中等 | 良好 | 优秀 |
6.2 可视化库对比
| 库 | 类型 | 交互性 | 适用场景 | 输出格式 |
|---|---|---|---|---|
| Matplotlib | 命令式 | 静态 | 学术论文、通用 | PNG/SVG/PDF |
| Plotly | 命令式 | 强 | 交互式探索、仪表板 | HTML |
| Bokeh | 声明式 | 强 | 大数据可视化、Web | HTML |
| Altair | 声明式 | 中 | 统计图表、快速原型 | Vega-Lite JSON |
| Seaborn | 命令式 | 静态 | 统计可视化 | PNG/SVG |
| HoloViews | 声明式 | 强 | 探索性分析 | 多种 |
6.3 Notebook 格式对比
| 格式 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| .ipynb | 标准、富输出 | 二进制嵌入、Git 不友好 | 交互式开发 |
| .py (jupytext percent) | Git 友好、可执行 | 无输出 | 版本控制 |
| .md (jupytext markdown) | 可读、兼容 Markdown | 需转换 | 文档分享 |
| .Rmd (R Markdown) | 集成 knitr | R 生态为主 | R 用户 |
6.4 批量执行工具对比
| 工具 | 定位 | 参数化 | 并行 | 调度 | 适用场景 |
|---|---|---|---|---|---|
| nbconvert | 转换 + 执行 | 不支持 | 不支持 | 无 | 一次性导出 |
| papermill | 参数化执行 | 支持 | 支持 | 无 | 批量实验 |
| jupyter-nbclient | 编程式执行 | 不支持 | 自定义 | 无 | 集成到应用 |
| Apache Airflow + papermill | 工作流 | 支持 | 支持 | 支持 | 生产调度 |
| Prefect + papermill | 工作流 | 支持 | 支持 | 支持 | 现代数据流 |
7. 常见陷阱与反模式
7.1 反模式:单元格执行顺序混乱
问题:Notebook 中单元格的执行顺序与物理顺序不一致,导致状态不可预测。
# Cell 1
x = 10
# Cell 2
print(x) # 假设执行时 x=10
# Cell 3(在 Cell 2 之后修改了 x)
x = 20
# 重新执行 Cell 2 → 输出 20,而非 10
危害:他人打开 Notebook 后”Run All”可能得到不同结果,破坏可复现性。
修复:
- 始终从上到下顺序执行。
- 定期执行
Kernel → Restart & Run All验证一致性。 - 使用
jupytext将 Notebook 同步为纯 Python 脚本,便于审查执行顺序。
7.2 反模式:在 Notebook 中导入自身模块
问题:
# mymodule.py 在同一目录
import mymodule # 可能导入的是已缓存的旧版本
原因:Python 的模块缓存(sys.modules)会持有第一次导入的版本,修改 mymodule.py 后重新 import 不会生效。
修复:
# 方案一:使用 autoreload 扩展
%load_ext autoreload
%autoreload 2 # 自动重新加载所有模块
import mymodule
# 方案二:手动重新加载
import importlib
import mymodule
importlib.reload(mymodule)
7.3 反模式:隐式状态依赖
问题:单元格 A 初始化变量,单元格 B 使用,但 A 未执行时 B 报错。
# Cell A(忘记执行)
import pandas as pd
df = pd.read_csv('data.csv')
# Cell B(执行报错 NameError: df is not defined)
df.head()
修复:
- 使用
Kernel → Restart & Run All验证。 - 将相关逻辑合并到一个单元格或封装为函数。
- 使用
assert检查前置条件。
assert 'df' in dir(), "请先执行数据加载单元格"
df.head()
7.4 反模式:将 Notebook 提交到 Git 含输出
问题:Notebook 的输出可能包含:
- 大型图像(嵌入 PNG bytes)
- 敏感数据(DataFrame 显示了真实用户数据)
- 执行计数错乱(合并冲突频发)
修复:
- 使用
nbstripout自动剥离输出。
# 安装 nbstripout
pip install nbstripout
# 配置 Git 过滤器
nbstripout --install
# 之后 git commit 会自动剥离输出
- 配合
jupytext同步.py版本用于审查。
7.5 反模式:循环导入大对象
问题:
# 每次循环都加载大数据集
for file in files:
data = pd.read_csv(file) # 重复 I/O
process(data)
修复:预加载或缓存。
from functools import lru_cache
@lru_cache(maxsize=32)
def load_data(filepath):
return pd.read_csv(filepath)
for file in files:
data = load_data(file)
process(data)
7.6 反模式:在生产环境用 Notebook 做服务
问题:直接将 Notebook 作为 Web 服务暴露,存在:
- 状态污染(全局变量被多次请求修改)
- 性能差(单线程、启动慢)
- 安全风险(
!Shell 命令、eval)
修复:用 Voila 或 nbconvert 转换为无状态应用,或将逻辑提取到 .py 模块。
7.7 反模式:魔法命令滥用
问题:在 Notebook 中大量使用 %cd、%env 改变全局状态,导致:
- 路径依赖隐式状态
- 难以转换为脚本
修复:使用绝对路径与环境变量配置。
# 不推荐
%cd /data/project
df = pd.read_csv('input.csv')
# 推荐
from pathlib import Path
DATA_DIR = Path('/data/project')
df = pd.read_csv(DATA_DIR / 'input.csv')
7.8 反模式:过度依赖 Matplotlib 全局状态
问题:
plt.plot(x, y)
plt.plot(x, y2) # 默认添加到当前图,可能污染前一个图
修复:使用面向对象 API。
fig, ax = plt.subplots()
ax.plot(x, y)
ax.plot(x, y2)
8. 工程实践
8.1 可复现 Notebook 实践
"""
可复现 Notebook 最佳实践
"""
# Cell 1:环境信息
%load_ext watermark
%watermark -a "Author" -d -v -m -p numpy,pandas,matplotlib,scikit-learn
# Cell 2:随机种子
import numpy as np
import random
import os
RANDOM_SEED = 42
np.random.seed(RANDOM_SEED)
random.seed(RANDOM_SEED)
os.environ['PYTHONHASHSEED'] = str(RANDOM_SEED)
# Cell 3:路径配置(使用 pathlib)
from pathlib import Path
PROJECT_ROOT = Path.cwd()
DATA_DIR = PROJECT_ROOT / 'data'
OUTPUT_DIR = PROJECT_ROOT / 'output'
OUTPUT_DIR.mkdir(exist_ok=True)
# Cell 4:依赖导入
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# 配置 Matplotlib
plt.rcParams['figure.figsize'] = (10, 6)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# Cell 5:数据加载与验证
df = pd.read_csv(DATA_DIR / 'iris.csv')
assert not df.isnull().any().any(), "数据存在缺失值"
print(f"数据形状: {df.shape}")
8.2 性能优化
"""
Jupyter 性能优化实践
"""
# 1. 使用 Cython 加速
%load_ext Cython
%%cython
def fib_cython(int n):
cdef int i
cdef double a = 0, b = 1
for i in range(n):
a, b = b, a + b
return a
# 2. 使用 numba JIT 编译
from numba import jit
@jit(nopython=True)
def fib_numba(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
# 3. 并行计算
from joblib import Parallel, delayed
def process_item(item):
# 耗时处理
return item ** 2
results = Parallel(n_jobs=-1)(delayed(process_item)(i) for i in range(1000))
# 4. 内存映射大文件
import numpy as np
big_array = np.memmap('large.dat', dtype='float32', mode='r', shape=(10000, 10000))
8.3 jupytext 版本控制
# 安装 jupytext
pip install jupytext
# 配置 Git 钩子:自动同步 .ipynb 与 .py
# 在 .gitattributes 中添加:
# *.ipynb filter=jupytext
# 在 Git 配置中添加:
git config filter.jupytext.clean 'jupytext --to py:percent'
git config filter.jupytext.smudge 'jupytext --to ipynb'
# 或手动同步
jupytext --sync analysis.ipynb
# 生成 analysis.py(percent 格式)
8.4 JupyterHub 部署
# jupyterhub_config.py 核心配置
c.JupyterHub.admin_access = True
c.JupyterHub.admin_users = {'admin'}
c.JupyterHub.spawner_class = 'jupyterhub.spawner.LocalProcessSpawner'
# 认证
c.JupyterHub.authenticator_class = 'jupyterhub.auth.PAMAuthenticator'
# 数据库
c.JupyterHub.db_url = 'sqlite:///jupyterhub.sqlite'
# 服务
c.JupyterHub.services = [
{
'name': 'idle-culler',
'command': [sys.executable, '-m', 'jupyterhub_idle_culler', '--timeout=3600'],
'admin': True,
}
]
Kubernetes 部署使用 zero-to-jupyterhub Helm Chart:
# values.yaml
proxy:
secretToken: "..."
service:
type: LoadBalancer
auth:
type: github
github:
clientId: "..."
clientSecret: "..."
callbackUrl: "https://jupyter.example.com/hub/oauth_callback"
singleuser:
image:
name: jupyter/datascience-notebook
tag: latest
cpu:
limit: 2
guarantee: 0.5
memory:
limit: 4G
guarantee: 1G
storage:
type: dynamic
capacity: 10Gi
9. 案例研究
9.1 案例:数据科学团队的可复现研究平台
场景:某 AI 公司的数据科学团队有 20 名成员,使用 Jupyter 进行模型实验,遇到:
- 实验结果不可复现(同事的 Notebook 在自己机器跑不通)
- 模型版本混乱(哪个 Notebook 对应线上 v1.2 模型?)
- 计算资源争抢(重型训练拖慢他人)
解决方案:
- 统一环境:使用 Docker 镜像
company/datascience-notebook:1.0,包含固定的 Python 版本与依赖。 - 存储共享:通过 NFS 共享数据集,避免每人各下载一份。
- JupyterHub + Kubernetes:每人独立容器,资源隔离(2 CPU / 4GB 内存)。
- papermill + MLflow:参数化执行 Notebook 并记录到 MLflow,关联模型版本。
- Git + jupytext:Notebook 同步为
.py进行代码审查。
收益:
- 实验复现率从 40% 提升至 95%
- 模型版本与代码可追溯
- 资源利用率提升 30%(自动 culler 回收空闲 Kernel)
9.2 案例:Notebook 导致的 OOM
场景:数据科学家在 Notebook 中加载 50GB 的 CSV 文件,导致 Kernel 崩溃。
根因:
# 错误做法:一次性加载
df = pd.read_csv('huge.csv') # 50GB CSV → 内存约 200GB(解析后膨胀 4 倍)
修复:
# 方案一:分块读取
chunks = pd.read_csv('huge.csv', chunksize=100000)
for chunk in chunks:
process(chunk)
# 方案二:指定 dtype 减少内存
df = pd.read_csv('huge.csv', dtype={
'id': 'int32',
'value': 'float32',
'category': 'category'
})
# 方案三:使用 Dask 分布式
import dask.dataframe as dd
ddf = dd.read_csv('huge.csv')
result = ddf.groupby('category').sum().compute()
# 方案四:使用 Modin(替代 Pandas,自动并行)
import modin.pandas as pd
df = pd.read_csv('huge.csv')
9.3 案例:Notebook 生产化部署
场景:数据科学家开发了一个数据清洗 Notebook,需要每日定时执行并生成报表。
错误做法:直接用 cron 调度 jupyter nbconvert --execute。
问题:
- 失败无告警
- 无法重试
- 输出无法管理
- 日志难查看
正确做法:使用 papermill + Airflow。
# Airflow DAG
from airflow import DAG
from airflow.operators.python import PythonOperator
import papermill as pm
from datetime import datetime, timedelta
default_args = {
'owner': 'data-team',
'depends_on_past': False,
'retries': 3,
'retry_delay': timedelta(minutes=5),
}
dag = DAG(
'daily_report',
default_args=default_args,
schedule_interval='0 8 * * *',
start_date=datetime(2026, 1, 1),
)
def run_notebook(**context):
"""执行 Notebook 并保存结果"""
ds = context['ds'] # 逻辑日期
pm.execute_notebook(
input_path='/notebooks/daily_report_template.ipynb',
output_path=f'/output/daily_report_{ds}.ipynb',
parameters={'date': ds}
)
task = PythonOperator(
task_id='run_report',
python_callable=run_notebook,
provide_context=True,
dag=dag,
)
9.4 案例:自定义 SQL Kernel
场景:数据分析师希望直接在 Jupyter 中编写 SQL 查询数据库,而非嵌套 Python。
实现:使用 ipython-sql 或自定义 Kernel。
# 使用 ipython-sql
%load_ext sql
%sql mysql+pymysql://user:pass@localhost/db
# 直接执行 SQL
%%sql
SELECT category, COUNT(*) AS count
FROM products
GROUP BY category
ORDER BY count DESC
LIMIT 10;
# 结果自动渲染为表格
# 赋值给变量
result = %sql SELECT * FROM products WHERE price > 100
df = result.DataFrame() # 转为 Pandas DataFrame
10. 习题
10.1 基础题
题目 1:解释 Jupyter 的前端与内核分离架构的优势。
参考答案要点:
- 语言无关:同一前端支持任意语言内核。
- 解耦开发:前端与内核可独立升级。
- 远程执行:内核可运行在远程服务器,前端在本地浏览器。
- 多语言协作:同一 JupyterLab 可切换不同语言内核。
题目 2:列举 5 个常用魔法命令并说明用途。
参考答案要点:
%timeit:多次执行测量平均耗时%prun:性能剖析%debug:事后调试%matplotlib inline:内联显示图表%%writefile:将单元格内容写入文件
题目 3:.ipynb 文件的本质是什么?
参考答案要点:
- JSON 格式的文本文件
- 包含
cells(单元格列表)、metadata(元数据)、nbformat(版本) - code 单元格含
source(代码)、outputs(输出)、execution_count(执行序号) - 输出可包含文本、HTML、图像(base64 编码)
10.2 进阶题
题目 4:详细解释 Jupyter Message Protocol 的 ZeroMQ 套接字模型。
参考答案要点:
- 四个套接字:Shell(请求-响应)、IOPub(广播)、Stdin(输入请求)、Control(控制)
- Shell 与 Control 都是 ROUTER/DEALER,但 Control 独立线程,可中断 Shell
- IOPub 是 PUB/SUB,所有前端订阅同一 Kernel 的输出
- 消息格式:header、parent_header、metadata、content、buffers
题目 5:如何解决 Notebook 的版本控制难题?
参考答案要点:
- 输出剥离:
nbstripout自动剥离输出,避免二进制冲突 - 格式转换:
jupytext同步.py/.md,便于 diff 与 merge - 执行计数清理:统一为 0 或剥离
- 元数据忽略:在
.gitattributes中配置 - 代码审查:审查
.py版本,而非.ipynb
题目 6:对比 Matplotlib 与 Plotly 在 Jupyter 中的使用。
参考答案要点:
- Matplotlib:静态图、命令式、学术友好、内联显示
- Plotly:交互式(缩放/悬停)、HTML 输出、适合探索
- Matplotlib 适合论文、报告;Plotly 适合仪表板、交互探索
- Matplotlib 全局状态易污染,Plotly 面向对象更清晰
10.3 挑战题
题目 7:设计一个基于 Jupyter 的数据科学团队协作平台,要求支持:
- 多用户隔离(每人独立环境)
- 共享数据集与模型
- 实验可追溯(参数、代码、结果关联)
- 资源限制(CPU、内存、GPU)
参考答案要点:
- JupyterHub + Kubernetes:多用户隔离,每用户独立 Pod
- 共享存储:NFS 或 S3 挂载数据目录
- 实验追踪:papermill 参数化 + MLflow 记录
- 资源限制:Kubernetes ResourceQuota + 单 Pod limit
- 认证:OAuth 集成 GitHub/Google
- 监控:Prometheus + Grafana 监控资源使用
题目 8:实现一个自定义 Jupyter Kernel,支持在 Notebook 中直接编写 SQL 查询 PostgreSQL,要求:
- 语法高亮
- 结果自动渲染为表格
- 支持
:variable引用 Python 变量(通过%sql魔法实现)
参考答案要点:
- 继承
ipykernel.kernelbase.Kernel - 实现
do_execute:解析 SQL、连接 PostgreSQL、执行、格式化结果 - 使用
pandas.read_sql将结果转为 DataFrame - 通过
_repr_html_自动渲染表格 - 注册 kernelspec(
kernel.json) - 变量替换:正则匹配
:var并从user_ns获取值
题目 9:分析 Notebook 在生产环境中的风险,并提出缓解方案。
参考答案要点:
风险:
- 状态污染(全局变量跨请求)
- 性能差(单线程、启动慢)
- 安全(Shell 命令、eval)
- 可维护性差(无单元测试、无类型检查)
缓解:
- 转换为
.py模块,编写单元测试 - 用 Voila 或 Streamlit 转换为无状态应用
- 用 papermill + Airflow 调度,而非直接暴露 Notebook
- 敏感操作(Shell 命令)移除或限制权限
11. 参考文献
[1] Pérez, F., & Granger, B. E. (2007). IPython: A System for Interactive Scientific Computing. Computing in Science and Engineering, 9(3), 21-29. DOI: 10.1109/MCSE.2007.53.
[2] Kluyver, T., Ragan-Kelley, B., Pérez, F., Granger, B., Bussonnier, M., Frederic, J., et al. (2016). Jupyter Notebooks - A Publishing Format for Reproducible Computational Workflows. In Positioning and Power in Academic Publishing: Players, Agents and Agendas, pp. 87-90. DOI: 10.3233/978-1-61499-649-1-87.
[3] Granger, B. E., & Pérez, F. (2021). Jupyter: Thinking and Storytelling With Code and Data. Computing in Science and Engineering, 23(2), 7-14. DOI: 10.1109/MCSE.2021.3059263.
[4] Project Jupyter. (2024). Jupyter Documentation. Retrieved from https://docs.jupyter.org/
[5] Ragan-Kelley, M., Perez, F., Granger, B., Kluyver, T., Ivanov, P., Frederic, J., & Bussonnier, M. (2014). The Jupyter/IPython architecture: a unified view of computational publication, collaboration and reproducibility. Proceedings of the 13th Python in Science Conference (SciPy). DOI: 10.25080/Majora-98bf1922-00e.
[6] Holdgraf, C. (2018). JupyterLab: The Next Generation Jupyter Web Interface. Journal of Open Source Software, 3(25), 656. DOI: 10.21105/joss.00656.
[7] Pimentel, J. F., Murta, L., Braganholo, V., & Freire, J. (2019). A Large-Scale Study About Quality and Reproducibility of Jupyter Notebooks. In Proceedings of the 16th International Conference on Mining Software Repositories (MSR), pp. 507-517. DOI: 10.1109/MSR.2019.00077.
[8] Rule, A., Tabard, A., & Hollan, J. D. (2018). Exploration and Explanation in Computational Notebooks. In Proceedings of the 2018 CHI Conference on Human Factors in Computing Systems, pp. 1-12. DOI: 10.1145/3173574.3173606.
[9] Yang, C., Poon, C., & Poon, K. (2019). Jupyter Notebooks for Data Science: A Pedagogical Study. Journal of Computing Sciences in Colleges, 34(6), 124-131.
[10] Uritsky, V. M., & Zhang, J. (2020). Interactive Computing with Jupyter Notebook in Materials Science. Computational Materials Science, 178, 109613. DOI: 10.1016/j.commatsci.2020.109613.
[11] Bhatti, A., & Kocak, D. D. (2021). Reproducible Research with Jupyter Notebooks. Patterns, 2(6), 100247. DOI: 10.1016/j.patter.2021.100247.
[12] Freeman, G., & Ross, K. A. (2019). JupyterHub for Education: A Deployment Guide. In Proceedings of the 50th ACM Technical Symposium on Computer Science Education (SIGCSE), pp. 1244-1250. DOI: 10.1145/3287324.3287440.
[13] Hunt, P. (2018). Enterprise Gateway: Scaling Jupyter Kernels to the Cluster. Jupyter Blog. Retrieved from https://jupyter.org/enterprise_gateway
[14] Chacon, S., & Straub, B. (2014). Pro Git. Apress. DOI: 10.1007/978-1-4842-0076-6.
[15] McKinney, W. (2017). Python for Data Analysis: Data Wrangling with Pandas, NumPy, and IPython. O’Reilly Media. ISBN: 978-1491957660.
12. 延伸阅读
12.1 官方文档
- Project Jupyter: https://jupyter.org/
- Jupyter Documentation: https://docs.jupyter.org/
- IPython Documentation: https://ipython.readthedocs.io/
- JupyterLab Documentation: https://jupyterlab.readthedocs.io/
- ipywidgets Documentation: https://ipywidgets.readthedocs.io/
12.2 经典教材
- Wes McKinney, Python for Data Analysis(数据分析必备)
- Jake VanderPlas, Python Data Science Handbook(数据科学全栈,含 Jupyter 实践)
- Cyrille Rossant, IPython Interactive Computing and Visualization Cookbook(IPython 深度)
12.3 源码与进阶
- Jupyter 源码: https://github.com/jupyter/
- JupyterHub: https://github.com/jupyterhub/jupyterhub
- papermill: https://github.com/nteract/papermill
- jupytext: https://github.com/mwouts/jupytext
- nbstripout: https://github.com/kynan/nbstripout
12.4 前沿论文与文章
- A Large-Scale Study About Quality and Reproducibility of Jupyter Notebooks(Notebook 质量研究)
- Exploration and Explanation in Computational Notebooks(Notebook 使用行为研究)
- JEP 444: Virtual Threads(与 Kernel 并发优化相关)
- Voila: From Notebooks to Dashboards(Notebook 应用化)
12.5 相关主题
- Python 与设计模式:Notebook 中的设计模式应用
- Python 与打包发布:将 Notebook 打包为可执行应用
- Python 与虚拟环境:Jupyter 内核与虚拟环境的集成
- Python 与代码质量:Notebook 的 lint、测试、类型检查
- Python 与 Redis:在 Notebook 中交互式探索 Redis 数据