1. 先搞清楚:Jupyter 里到底有几个“环境”
第一次被 Jupyter 的环境问题坑到,是我在一个新机器上装完 Anaconda,兴冲冲打开 Notebook,import torch报 ModuleNotFoundError。我明明在终端里pip install过了,怎么就不认?后来才发现,我装包的那个 Python 和 Notebook 单元格里跑的 Python,压根不是同一个解释器。这件事让我意识到,聊“在 Jupyter Notebook 中切换环境”之前,得先把“环境”这个词拆开——Jupyter 生态里,它至少有三层含义,混在一起就必然踩坑。
第一层是启动环境(server 环境)。你在终端敲jupyter notebook或者jupyter lab的那一刻,用的是哪条命令链上的 Python,那个 Python 就负责跑 Jupyter 服务端本身,包括 Tornado、nbformat、nbconvert 这些组件。这个环境决定了 Notebook 能不能启动、界面版本是多少、装没装扩展插件。
第二层是内核环境(kernel 环境)。这才是你写import pandas时真正执行代码的那个 Python 解释器。它可能和启动环境完全无关:你完全可以用 base 环境启动 Jupyter,却在单元格里跑一个 Python 3.11 的 conda 环境。切换环境,九成九指的是切换这个。
第三层是虚拟环境管理器产物(conda env / venv / uv venv)。它们是磁盘上一堆互相隔离的 site-packages 目录,本身不是内核,只有被“注册”成 kernel 之后,Jupyter 才能在界面上看到它。
搞清楚这三层,很多玄学问题就自动破案了。典型症状:jupyter notebook打不开、或者打开是空白页,属于第一层;单元格里 import 报错,属于第二层;jupyter kernelspec list里找不到你刚建的环境,属于第三层。后面我讲的所有操作,基本都是在第二层和第三层之间搭桥。
1.1 内核是怎么被 Jupyter “看见”的
Jupyter 不需要把环境装进自己身体里,它只需要一份“简历”。这份简历就是kernel.json,放在内核规格目录下:
- Windows:
%APPDATA%\jupyter\kernels\<kernel-name>\ - Linux / macOS:
~/.local/share/jupyter/kernels/<kernel-name>/
文件内容长这样:
{ "argv": [ "C:\\Users\\me\\miniconda3\\envs\\py311\\python.exe", "-m", "ipykernel_launcher", "-f", "{connection_file}" ], "display_name": "Python (py311)", "language": "python" }关键就在argv的第一项:它写死了哪个 python.exe。Jupyter 启动内核时,就用这个解释器去跑ipykernel_launcher。所以只要这份简历里的路径指向你想要的虚拟环境,单元格里就是那个环境。display_name只是界面上给你看的名字,改它不影响实际解释器——这一点非常容易误判,很多人改完显示名以为换环境成功了,其实底层 Python 一动没动。
内核规格目录分用户级和系统级。--user参数写的是用户级,也就是上面那两个路径;不加--user就是系统级,装在 Python 的share/jupyter/kernels下。多用户机器上建议统一用--user,避免权限问题和相互覆盖。
1.2 为什么“我 pip 装了却 import 不到”
这是新手最痛的一刀。你打开终端,pip install requests,提示成功;回到 Notebook,import requests照样失败。原因很朴素:终端的pip属于 base 环境,而 Notebook 单元格跑的是另一个环境的 python。pip 装包只作用于它自己绑定的那个解释器。
判断方法很简单,在单元格里跑一行:
import sys print(sys.executable)把它和终端里which python(Windows 上是where python)的结果对一下,是不是同一个路径。不一致,就说明你装错地方了。这也是为什么我后来养成习惯,装包一律用单元格内的魔法命令:
%pip install requests%pip会把包精确装进当前内核对应的解释器,从根上杜绝“装错环境”。conda 环境还可以用%conda install xxx。这两个魔法命令,是我认为值得贴在显示器上的两条命令。
2. 三种主流切换方案,选对能省一半时间
方案没有绝对好坏,取决于你的环境管理习惯。我按使用频率和稳定度排一下,你可以直接对号入座。
2.1 手动注册 ipykernel:最稳、最通用
思路就是给每个环境装一个ipykernel,然后手动注册。好处是可控,你清楚地知道哪个环境对应哪个内核,出问题也容易定位。坏处是每次新建环境都得手动来一遍。
conda activate py311 pip install ipykernel python -m ipykernel install --user --name py311 --display-name "Python (py311)"--name是内核在文件系统里的标识,建议用英文、无空格;--display-name是界面上显示的名字,可以带中文和空格。注册完刷新浏览器,内核列表里就能看到。
2.2 nb_conda_kernels:新建环境自动出现
如果你环境特别多,又懒得一个个注册,可以装这个插件:
conda install -n base nb_conda_kernels它的原理是启动 Jupyter 时扫描所有 conda 环境,凡是装了ipykernel的环境自动变成可选内核。注意那个前提——目标环境里必须有 ipykernel,否则扫不到。所以正确姿势是两个环境分别装:
conda install -n base nb_conda_kernels conda install -n py311 ipykernel这个方案的坑在于:某些版本的 conda 和 Jupyter 组合会导致启动变慢,环境多了之后每次开 Notebook 要等好几秒扫描。另外它只管 conda 环境,venv 和 uv 建的环境它看不见。
2.3 uv + venv 注册:现在我最常用的
uv 建环境快得离谱,几秒钟的事,很适合做临时实验。它建出来的就是标准 venv,注册方式也是标准的:
uv venv .venv --python 3.12 .venv\Scripts\activate # Windows source .venv/bin/activate # Linux / macOS uv pip install ipykernel python -m ipykernel install --user --name uv-py312 --display-name "Python (uv 3.12)"如果你用 pyproject.toml 管理依赖,也可以uv add ipykernel把它记为项目依赖,然后再执行注册命令。注册完之后,.venv删掉之前记得把内核也删掉,不然界面上会留一个指向不存在路径的死内核。
三种方案对比如下:
| 方案 | 适用场景 | 优点 | 注意点 |
|---|---|---|---|
| 手动 ipykernel | 环境数量不多、追求可控 | 明确、稳定、跨管理器通用 | 每个环境都要手动注册 |
| nb_conda_kernels | conda 环境多、图省事 | 自动发现、零维护 | 仅支持 conda;启动略慢 |
| uv / venv + ipykernel | 临时实验、快速切换 | 建环境极快、轻量 | 需手动注册与清理 |
3. 手把手实操:把每个环境都挂上 Jupyter
理论讲完,直接上手。我按“从干净机器到能切环境”的完整链路走一遍,你照着敲就行。
3.1 conda 环境的完整注册流程
假设你已经装了 Miniconda 或 Anaconda。第一步建环境:
conda create -n dl-py311 python=3.11 conda activate dl-py311第二步装 ipykernel,这是必须的,没有它注册不了:
pip install ipykernel第三步注册。这里有个细节:注册时要确认当前激活的就是目标环境,否则容易把内核路径写错。注册之后立刻验证:
python -m ipykernel install --user --name dl-py311 --display-name "Python (dl-py311)" jupyter kernelspec list输出里应该能看到dl-py311以及它对应的路径。路径末尾是kernels\dl-py311,前面那一长串应该是你 conda 环境的路径。如果路径指向 base,说明你注册时没激活对环境,删掉重来。
3.2 验证内核到底连的是哪个 Python
切完内核,别急着写业务代码,先跑一段“体检代码”:
import sys, os print("executable:", sys.executable) print("version:", sys.version) print("conda env:", os.environ.get("CONDA_PREFIX")) print("sys.path[0:3]:", sys.path[:3])executable会告诉你真实解释器路径;CONDA_PREFIX在 conda 环境下会打印环境根目录,venv 下通常是空的。这一步能挡掉八成“以为切了其实没切”的乌龙。我见过有人注册时--name用重复的名字,结果新内核覆盖了旧内核的简历,界面上两个名字长得一样,实际都指向同一个解释器,查半天查不出来。
3.3 删除、重命名与常见收尾动作
内核不是注册完就永远对的,环境删了、路径挪了,都会留下死内核。删除很简单:
jupyter kernelspec remove dl-py311重命名稍微绕一点,因为jupyter kernelspec没有 rename 子命令。两个办法:一是直接编辑那份kernel.json改display_name,改完重启 Jupyter 界面生效;二是删掉重新注册。我一般用第二种,干净。
还有一个高频需求:注册内核时不想用--user,想装到系统级让所有用户都能用,那就去掉--user参数,但要确保你有写权限。Windows 上如果 PermissionError,多半是没管理员权限,加回--user就行。
注意:
--name一旦定下来尽量别改,因为有些脚本、配置文件里会引用这个名字。显示名可以随便改,标识名要慎重。
4. Windows 上的专属坑:DLL 报错与启动失败
Windows 用户遇到的 Jupyter 环境问题,硬是要比 Linux 多一截。我把最典型的两个单独拎出来说。
4.1 importerror: dll load failed while importing rpds
这个报错全称一般是ImportError: DLL load failed while importing rpds: 找不到指定的模块。rpds是 rpds-py,一个用 Rust 写的库,被 jsonschema 依赖,而 nbformat、jupyter 全家桶又依赖 jsonschema。所以它一挂,Jupyter 直接起不来。
报错本质是:Python 在加载rpds的.pyd扩展模块时,找不到它需要的底层 DLL。常见原因有这么几个。
一是环境和解释器混装。你在 conda 环境里用 pip 装了 rpds-py,但那个 wheel 可能和当前 Python 版本、位数不匹配。最典型的是 32 位 Python 混进 64 位环境。
二是缺 Visual C++ 运行库。Rust 编译的扩展在 Windows 上依赖 VC++ 运行时,机器上没装或者版本太老就会加载失败。去装一个最新的 Microsoft Visual C++ Redistributable(x64)通常能解决。
三是pip 缓存里有坏 wheel。解决办法是强制重装:
pip install --force-reinstall --no-cache-dir rpds-py四是conda 和 pip 混合安装导致的依赖冲突。conda 装的 jsonschema 和 pip 装的 rpds-py 版本对不上。我的经验是:一个环境里尽量别混用,要么全 conda,要么全 pip。实在要混,先卸载 rpds-py 和 jsonschema,再统一用 pip 重装。
排查顺序我总结成一张表,遇到就按这个走:
| 步骤 | 操作 | 判断依据 |
|---|---|---|
| 1 | python -c "import sys; print(sys.version, sys.maxsize > 2**32)" | 确认 64 位 |
| 2 | 安装最新 VC++ Redistributable x64 | 补齐运行时依赖 |
| 3 | pip uninstall rpds-py jsonschema后重装 | 清除冲突版本 |
| 4 | pip install --force-reinstall --no-cache-dir rpds-py | 绕过坏缓存 |
| 5 | 检查是否 conda/pip 混装 | 统一包管理来源 |
4.2 jupyter notebook 打不开、无法运行
“打不开”分两种情况:命令执行后终端报错,或者命令没报错但浏览器空白。
终端报错的,先看报错里有没有ImportError、DLL load failed,有就按上面那套走。如果报的是ModuleNotFoundError: No module named 'jupyter',说明你当前环境根本没装 Jupyter,或者 PATH 指向了别的 Python。用where jupyter(Windows)确认命令行找到的是哪个可执行文件。
浏览器空白的,先手动在终端看它打印的地址,通常是http://localhost:8888/tree?token=xxx,把带 token 的完整地址复制进浏览器。空白页多半是 token 没带或者端口被占。端口被占的话,换一个:
jupyter notebook --port 8899还有一种“执行单元格没反应”的情况,看着像卡住,其实内核已经死了。表现是单元格左侧变成[*]一直不消失。这时候别干等,菜单里 Kernel → Restart,或者 Interrupt。如果重启也不行,去终端看 Jupyter 的输出,通常能看到内核崩溃的堆栈。常见诱因是某个包在导入时段错误(比如底层 C 扩展和当前 Python 不兼容),换环境或降版本就好。
4.3 多版本环境切换的通用心法
顺手聊个题外话,但思路完全通用。Windows 上做 Java 开发的人经常要在 JDK 11 和 JDK 21 之间切,做法无非是改JAVA_HOME和PATH,或者写两个.bat脚本一键切。这套逻辑和 Jupyter 切换环境本质一样:不改全局默认,只改当前作用域的解释器指向。
区别在于,JDK 切换是进程级的(你开一个新终端就换了),而 Jupyter 是“一份简历对应一个解释器”,切换动作发生在界面上选内核那一刻。理解了这套隔离思维,Python 环境、Node 版本(nvm)、JDK 版本,你都会切换得明明白白。我在实践里的体会是:凡是能用“作用域隔离”解决的版本问题,都不要去动系统全局变量,改全局是万恶之源。
5. 把 Jupyter 体验拉满:补全、目录与执行问题
环境切好了只是及格线,真正顺手还得配几个东西。
5.1 代码自动补齐怎么配
Jupyter Notebook 7 和 JupyterLab 内置了基于内核的补全(按 Tab 触发),开箱即用,但只能补当前内核已知的符号,智能程度一般。想要“类 IDE”体验,装 LSP:
pip install jupyterlab-lsp python-lsp-server[all]装完重启 JupyterLab,就会有实时的函数签名提示、跳转定义、悬停文档。注意jupyterlab-lsp只对 JupyterLab 和 Notebook 7 生效,经典的 Notebook 6 不认。如果你还在用 Notebook 6,补全要靠jupyter-contrib-nbextensions里的 Hinterland,但这个项目维护已经不太活跃了,我的建议是直接升到 Notebook 7,别在旧版本上耗。
还有一个细节:LSP 默认可能只对当前 kernelspec 的语言生效。如果你装的是 R 内核,那 LSP 补全就未必对得上,得单独配置 language server。
5.2 Markdown 目录生成语法
严格说,Markdown 本身没有原生的目录语法。想要目录,有三种做法。
第一种是手写锚点加链接:
<a id="sec1"></a> ## 第一节 [跳转到第一节](#sec1)第二种是借助 Notebook 扩展。装了 Table of Contents (2) 扩展后,左侧会有一个自动生成的目录侧栏,标题层级自动识别,点一下就能跳。Notebook 7 和 JupyterLab 自带这个侧栏,不用额外装。
第三种是导出时用 nbconvert 的 toc 模板,生成带目录的 HTML:
jupyter nbconvert --to html --template toc2 notebook.ipynb我自己的习惯是:写长文档时用侧栏目录,导出分享时用 nbconvert 模板,两边都省事。手写锚点只适合小文档,标题一多就维护不动了。
5.3 单元格执行没有任何反应,怎么排查
前面提过一部分,这里给一套完整排查顺序。先看单元格左侧的序号:如果是[*]长时间不动,说明内核忙碌或已死。先点 Interrupt,不行再 Restart Kernel。重启后如果依然执行无反应,去终端看 Jupyter 输出,有堆栈就看堆栈。
如果序号根本没变,说明这条命令压根没发出去,通常是前端问题——浏览器扩展拦截、缓存陈旧。强制刷新(Ctrl+F5)或者换个浏览器试试。还有一种低概率情况:单元格类型被改成了 Raw,Raw 单元格执行时什么都不做。看左上角下拉框是不是 Raw,改回 Code 即可。
顺带说个高频问题:jupyter notebook 安装之后找不到命令,多半是 Scripts 目录没进 PATH。Windows 上 pip 安装的可执行文件放在Python安装目录\Scripts,把它加进系统 PATH,或者直接用python -m notebook启动,绕过 PATH 问题。
最后一个经常被问的:jupyter notebook nvim组合。有人喜欢在 Neovim 里写代码再丢进 Notebook 跑。做法是用 jupytext 把.py文件当 notebook 打开,在 Neovim 里编辑,保存后 Jupyter 自动刷新。核心是装 jupytext 并在 Notebook 里启用对应扩展,环境层面不需要额外折腾,前提还是那句话——确保你编辑文件时用的环境和内核环境一致。
6. 常见问题速查表与几句实在话
把上面这些症状、原因、解法压成一张表,收藏起来遇到就查:
| 现象 | 大概率原因 | 解决方向 |
|---|---|---|
| import 不到已安装的包 | 包装到了别的环境 | 用%pip install或核对sys.executable |
| 内核列表看不到新环境 | 没注册或没装 ipykernel | 执行ipykernel install --user |
| 切换内核后仍是旧解释器 | 内核简历路径指错 | 检查 kernel.json 的 argv |
| rpds DLL 加载失败 | 缺运行库或包冲突 | 装 VC++ 运行库、强制重装 |
| Notebook 打不开 | 端口占用或 token 丢失 | 换端口、复制带 token 的完整地址 |
单元格一直[*] | 内核崩溃或死锁 | Interrupt / Restart,看终端堆栈 |
| 补全不生效 | 旧版 Notebook 或未装 LSP | 升级到 Notebook 7,装 jupyterlab-lsp |
| Markdown 没有目录 | 缺扩展或未用模板 | ToC 侧栏或 nbconvert toc 模板 |
最后分享一个我自己一直在用的习惯:每建一个新环境,先跑三件事——装 ipykernel、注册内核、在单元格里打印sys.executable核对路径。三步花不到一分钟,能避免后面几小时的排查。环境这东西,隔离得越干净,日子过得越舒服。