上个月清理笔记本磁盘的时候,我发现一个一直想处理但始终没动手的问题:Anaconda目录整整占了16G,而且每次conda create一个干净环境都要等上快半分钟,依赖解析更是动不动就卡住一两分钟。正好那段时间看到很多人在聊 uv,说它是用 Rust 写的下一代 Python 包管理工具,速度极快,能同时替代 conda、pip、pip-tools、virtualenv 这一堆工具。我心想,反正之前的习惯也到了该重构的时候,干脆把主力环境从 Anaconda 切到 uv 试试。
这一试就是三个月,期间踩了不少坑,也改掉了几个长期依赖 conda 养成的坏习惯。如果你也在纠结“要不要从 Anaconda 换到 uv”,或者已经装了 uv 但不知道从哪下手,这篇文章应该能帮到你。我会先讲清楚为什么值得换,再把完整的迁移步骤、命令对照和坑点都列出来,最后聊聊换完之后工作流发生了哪些变化。
1. 换之前先想清楚:conda 真正让你难受的点是什么
1.1 环境管理到底在解决什么问题
Python 的包管理乱不是一天两天了。不同项目依赖同一个库的不同版本,A 项目要 requests 2.x,B 项目要 requests 3.x,直接装进全局环境很快就会互相打架。所以虚拟环境这个概念核心就一件事:隔离,把每个项目的依赖装进各自独立的目录。
conda 在这个领域的历史地位不用我多说,它以前最强的点是能分发二进制包,不光是 Python 库,连底层的 C 库、CUDA 驱动这种非 Python 组件都能一起管理。所以在数据科学、机器学习这个圈子里,Anaconda 几乎成了默认标配。我在学校里第一次装 Python 环境就是 Anaconda,那时候确实省心:一个安装包搞定 Python、Jupyter、numpy、pandas,不用自己配一堆东西。
但“省心”是有时效的。随着项目越来越多、环境越建越多,conda 的重量级问题开始盖过它的便利性。我自己的笔记本上攒了十几个 conda 环境,每个环境都几百 MB 起,加上pkgs目录里的缓存,16G 就是这么攒出来的。平时用的时候不觉得,真到磁盘吃紧、换新机器、或者要复现一个三个月前的项目时,痛点就全冒出来了。
1.2 conda 用久了,我积累的四个真实痛点
我复盘了一下自己过去两年的使用记录,最频繁困扰我的不是某一个报错,而是下面四个反复出现的问题:
| 痛点 | 具体表现 | 对我日常工作的影响 |
|---|---|---|
| 依赖解析速度慢 | conda install要回溯历史版本、求解依赖冲突,经常卡在 Solving environment | 装一个包等一分多钟,打断思路;CI 里跑一次完整环境准备要耗掉大半任务时间 |
| 磁盘占用大 | 每个环境独立复制一份包,base 环境和缓存目录巨大 | 16G 只是 Anaconda 目录,还没算上各个项目.conda缓存 |
| 双包管理器混用 | 有些包 conda 没有,还得pip install,最终环境状态说不清楚 | 换机器时按 environment.yml 复现,经常差几个包,跑起来才报错 |
| 版本锁定能力弱 | environment.yml 可以锁版本,但默认写的是范围,不是精确哈希 | 团队协作时“我这能跑你那不行”的问题反复出现,没人说得清依赖到底锁没锁死 |
这两个问题叠加起来,就形成了一个很尴尬的状态:conda 看起来提供了全流程管理,但实际上它的环境描述文件既不能精确锁定传递依赖,也不能保证换一台机器 100% 复现。尤其是当你依赖了一堆用 pip 装进去的包之后,conda env export导出来的 yml 文件其实已经失真了。
我意识到,我真正需要的不是“更快的 conda”,而是一套能把环境定义、依赖锁定、虚拟环境创建、命令执行全链路管起来,而且每一步都足够快的工具。uv 就是在这个诉求下进入我视野的。
2. uv 改掉了什么:从解算器到缓存,再到项目文件
2.1 uv 为什么快:Rust 实现加上全局硬链接缓存
先说结论,uv 快主要靠两点。
第一点是它用 Rust 重写了依赖解析逻辑。conda 的 solver 要处理的东西很庞大,它得考虑 Python 版本、操作系统、非 Python 二进制包的依赖关系,搜索空间非常大;uv 目前专注在 PyPI 生态的 wheel 和源码包上,而且用并行下载、增量解析的方式,把“解析依赖”这个最耗时的环节压缩到了毫秒到秒级。用我自己的实测数据:uv add pandas在全新环境里大概 2-3 秒完成解析和安装,同样的操作 conda 至少要 30 秒起步,差距非常明显。
第二点是 uv 的全局缓存机制。它会在你用户目录下建一个统一的缓存区(~/Library/Caches/uv或~/.cache/uv),所有虚拟环境共享同一份下载过的包,安装新环境时通过硬链接把文件链接进.venv,而不是重新复制一份。这就是为什么我建了十几个 uv 项目,磁盘占用依然比之前一个 conda 环境小得多。
提示:uv 缓存的硬链接机制要求缓存和虚拟环境在同一个文件系统上,这个在本地机器上一般都满足。如果你把项目放到外接硬盘或网络盘上,缓存命中率会下降,速度也会打折。
2.2 pyproject.toml 取代 requirements.txt,依赖关系更明确
uv 把项目的依赖描述统一收敛到了pyproject.toml。以前 conda 时代,我的依赖分散在environment.yml、requirements.txt、手动装的包三处,时间一长根本记不清哪个是真依赖、哪个只是当时调试用的临时包。
pyproject.toml 的好处是它把“直接依赖”和“锁定版本”分开了:
[project.dependencies]里写的是直接依赖,可以给出版本范围,比如requests>=2.31;uv.lock文件负责精确锁定,它会记录每个直接依赖和传递依赖的具体版本、哈希值、来源。
日常改动时,我只管往 pyproject.toml 里加依赖,具体的版本解析交给 uv。每次需要精确复现环境时,用uv sync按 uv.lock 安装。这种“宽入口 + 严出口”的设计,比 requirements.txt 这种单文件既当声明又当锁定的做法清晰很多。
2.3 uv 自己管 Python 版本
这是 conda 时代我完全没想到的。uv 内置了 Python 版本管理能力,uv python install 3.12可以直接下载一个独立的 CPython 解释器,然后在项目根目录通过.python-version文件指定这个项目用哪个 Python 版本。uv venv --python 3.12创建虚拟环境时,会自动找到或下载对应版本的解释器。
这意味着我可以彻底告别 conda 里那套“创建环境时指定 python=3.8,之后想升级只能重建环境”的流程。换 Python 版本在 uv 里就是改一行.python-version然后重新uv sync的事。对我来说这是迁移之后体验提升最大的一点之一,后面实战部分我会专门演示。
3. 一步步把项目从 Anaconda 迁到 uv
3.1 安装 uv 和过渡期共存策略
安装 uv 非常简单,macOS/Linux 一行命令:
curl -LsSf https://astral.sh/uv/install.sh | shWindows 可以用 PowerShell 脚本或者直接winget install astral-sh.uv。装完之后uv --version确认一下即可。如果你之前用 pip,也可以pip install uv把它当普通工具先装上,之后再考虑要不要把 pip 也换掉。
我建议迁移初期不要删掉 Anaconda,先让两者共存一段时间。原因很实际:你手上也许有一些老脚本、旧项目,它们的环境还是 conda 管的,在那些项目彻底迁移完之前,删掉 conda 等于自断后路。共存没有任何冲突,uv 创建的环境都在各自项目目录下的.venv里,和 conda 的环境目录互不干扰。我自己的过渡期大约维持了三周,等所有活跃项目都切到 uv 之后才卸载 Anaconda。
3.2 我的迁移流程:从 environment.yml 到 pyproject.toml
我挑了一个中型的项目来做迁移实验,它大概有三十几个依赖,里面既有 numpy、pandas 这种常见的科学计算库,也有几个需要编译的包。整个迁移大概花了四十分钟,核心流程如下。
第一步,先把 conda 环境的真实依赖列表导出来,摸清家底:
conda env export -n myenv | grep -E "^- [A-Za-z]" | sed 's/^- //' | cut -d'=' -f1 > deps.txt这一步不是必须的,但它能让你看到这个环境里到底装了哪些直接依赖。注意conda env export导出的文件里有很多 conda 特有的包(比如 openssl、libgcc 这种底层库),这些不用管,uv 会把它们作为传递依赖自动处理。
第二步,在项目根目录初始化 pyproject.toml:
uv init如果项目已经存在,uv init会补一个最小化的 pyproject.toml,不会覆盖你已有的文件。
第三步,逐个添加依赖,把依赖分成运行依赖和开发依赖两类:
uv add numpy pandas scikit-learn uv add --dev pytest ruff ipykernel运行依赖写进[project.dependencies],开发依赖写进[dependency-groups.dev],语义非常清楚。这一步是最体现 uv 价值的地方:uv add会立刻解析依赖树并生成 uv.lock,不用等到最后再统一安装。
第四步,验证项目能不能正常跑:
uv run python main.pyuv run会自动检查.venv是否存在、依赖是否和锁文件一致,不一致会先同步再执行命令。所以这一步实际包含了“创建环境 + 安装依赖 + 运行脚本”三步。
迁移过程里最需要注意的细节是处理版本约束。conda 环境里很多包并不是最新版本,如果你在 pyproject.toml 里直接写numpy,uv 会默认装最新版,这可能导致项目里某个老接口直接崩掉。稳妥的做法是在uv add时就把版本上限写清楚:
uv add "numpy>=1.24,<2.0" uv add "pandas==2.0.3"3.3 日常命令对照表
如果你之前的主力工具是 conda,下面的对照表可以帮你快速上手。我不建议把这张表当成速查手册收藏了事,而是建议你实际跑一遍,建立肌肉记忆。
| 功能 | conda | uv |
|---|---|---|
| 创建虚拟环境 | conda create -n myenv python=3.12 | uv venv .venv --python 3.12 |
| 激活环境 | conda activate myenv | 不需要显式激活,用uv run即可 |
| 安装包 | conda install requests | uv add requests |
| 安装开发依赖 | conda install pytest -c conda-forge | uv add --dev pytest |
| 删除依赖 | conda remove requests | uv remove requests |
| 查看环境列表 | conda env list | 每个项目一个.venv,不集中列出 |
| 导出环境 | conda env export > env.yml | uv lock生成 uv.lock |
| 复现环境 | conda env create -f env.yml | uv sync |
| 切换 Python 版本 | 重建环境 | 修改.python-version后uv sync |
要特别强调一下“激活”这件事。uv 的设计哲学是:你不应该手动激活环境,而应该通过uv run来进入这个环境。第一次用会觉得不习惯,总觉得不 activate 就不放心。但用久了你会发现,不激活其实是好事——你永远不会忘记自己当前在哪个环境里,因为根本不存在“当前环境”这个全局状态。每个uv run都是临时进入项目环境执行一条命令,执行完回到系统环境。
4. 换过来之后,我踩过的坑和调整过的习惯
4.1 别把旧环境的包一股脑拷过来
我最早想的迁移方案比较粗暴:把 conda 环境里所有包列出来,然后uv pip install照着装一遍。结果失败了,一是有几个包在 conda 里存在但 PyPI 上根本没有,二是照单全装把一堆不该有的深层依赖也带进来了,环境从第一天起就是脏的。
后来我调整了策略:只迁移“代码里 import 过的包”到 pyproject.toml,其余的一律不装。这个思路虽然麻烦一点,但是值得的,因为你迁移的是一个项目的真实依赖,不是把你硬盘上所有环境合并成一个。
如果你实在没精力一个一个筛,可以用pipreqs之类的工具扫描项目代码里的 import 语句生成依赖列表,然后uv pip install -r 生成的requirements.txt作为一次性过渡。但这只是过渡方案,还是尽早把依赖收敛到 pyproject.toml 里比较好。
4.2 编译型依赖的第一次劝退
迁移过程中我第一次体验到 uv 的“快”的代价:它默认优先用 PyPI 的 wheel 预编译包,装得很爽;但遇到某些只发布源码包、没有发布 wheel 的库时,uv 会在本地现场编译,这时候如果机器上缺编译工具链,就会报出一堆 C/C++ 编译错误。
我遇到的那个包需要依赖libffi和 OpenSSL 的 dev 头文件,在 conda 时代这些都由 conda 的二进制包间接提供了,根本不用管。换到 uv 之后,需要自己保证系统里的编译工具链是完整的。macOS 上要装好 Xcode Command Line Tools,Ubuntu 上要apt install build-essential python3-dev之类的。
遇见这种情况不用慌,解决方案也很直接:要么给项目指定一个 PyPI 上已有对应 wheel 的 Python 版本,要么用--no-build-isolation配合系统里已有的编译环境去编译。我的建议是,如果某个库在 PyPI 上长期没有预编译的 wheel,同时你的项目又重度依赖它,那这个项目可以先留在 conda 里,没必要为了迁移而迁移。
4.3 IDE 解释器路径和缓存绕过
迁移完项目之后,我发现开发工具也得跟着调整。Visual Studio Code 之类的编辑器默认会找系统 Python 或 conda 环境,不会自动识别项目里的.venv,所以得手动设置解释器路径为项目根目录下的.venv/bin/python(Windows 是.venv\Scripts\python.exe)。
还有个比较容易忽略的小点:uv 可以配置镜像源来加速下载。如果是在公司内网或者国内网络环境下,PyPI 官方源可能不稳定,这时可以在环境变量里指定镜像:
export UV_DEFAULT_INDEX="https://mirrors.xxx.com/pypi/simple"如果你不想全局改,也可以在项目目录建一个.env文件或者在uv add时加--index-url参数。这个配置本身不难,难的是意识到“uv 的下载配置和 conda 是两套体系”。我在迁移初期经常下意识找.condarc的配置,后来才反应过来,这套配置文件早就跟 conda 一起退役了。
4.4 版本锁文件带来的“不自由”
刚开始用 uv.lock 那几天,我总觉得别扭:以前 conda 里想升级某个包,conda update 包名直接完事;uv 里每次 upgrade 都要过一遍锁文件,感觉被约束了。后来我在一次线上事故里才想明白,这种“约束”其实是保护。
那次事故是因为我手动升级了一个传递依赖,结果带动了另一个包的大版本变化,项目在测试环境躺了两小时。从那以后我彻底接受了锁文件的玩法。uv 里升级依赖也不复杂,分两种:
- 全部升级:
uv lock --upgrade重新解析所有依赖并更新锁文件; - 单独升级:
uv lock --upgrade-package requests只把 requests 以及受它影响的依赖一起升级。
锁定和升级是分开的两个动作,这在团队协作里特别管用。因为锁文件保证了所有人在同一时刻拿到的依赖树是一致的,而升级则是一个显式的、有意识的决策,而不是某个人在自己机器上偷偷pip install --upgrade之后产生的随机结果。
5. uv 带来的几个新工作流,用过就回不去了
5.1 uv tool 管理命令行工具,环境不再相互污染
以前装命令行工具(比如 ruff、httpie、poetry 等)我都是直接pip install到 base 环境里,这样做的坏处是 base 环境越来越臃肿,而且工具之间的依赖可能会互相覆盖。uv 里有uv tool install,它把每个命令行工具装进独立的工具环境,之后通过uv tool run执行,环境完全隔离。
uv tool install ruff uv tool run ruff check .还有更轻量的uvx,它用于“只临时跑一次某个包里带的 CLI”的场景:
uvx cowsay hellouvx会临时创建一个环境、安装 cowsay、执行命令、然后环境自动销毁。这种“用完即走”的模式,对提高日常效率帮助很大,我再也不需要为了跑一个小工具而污染任何环境。
5.2 一条命令跑脚本:uv run 脚本.py
uv 对单文件脚本的支持也很惊艳。以前我写一个处理数据的临时脚本,如果它依赖 pandas,我得先想清楚当前在哪环境、有没有 pandas、要不要装。现在 uv 支持 PEP 723 标准的内联依赖声明,在脚本顶部注释里写清楚依赖,然后直接uv run script.py,它会自动创建隔离环境并运行:
# /// script # dependencies = ["pandas>=2.0"] # /// import pandas as pd print(pd.__version__)uv run my_script.py依赖、环境、运行一条龙。我经常把一些需要临时用到特定库的分析脚本发给自己或同事,对方直接就能跑,不用再问“装了什么包”“用的哪个环境”。
5.3 CI 和团队协作中的改变
最后说说团队协作层面的变化。之前我用 conda 时,新同事入职把项目跑起来至少要 20 分钟,期间还经常出现“我明明按要求装了环境但还是缺包”的问题。换成 uv 之后,流程变得极其统一:
- 克隆仓库;
uv sync;- 跑测试
uv run pytest。
没了。环境创建、依赖安装、锁文件校验全部自动完成。我在 CI 里也只写两行:
uv sync --frozen uv run pytest--frozen参数是不允许修改锁文件,确保 CI 严格按 uv.lock 安装。如果发现有依赖漂移,CI 会直接报错,问题在代码审查阶段就被拦截了。
这在团队里带来的结果是:大家聊“环境问题”的时间骤减,不再有“我这能跑你那不行”的玄学。后来我们干脆把仓库里的 requirements.txt 都删掉了,所有依赖统一走 pyproject.toml 和 uv.lock,连文档都简洁了不少。
我个人目前最满意的状态是:包里不再有几十个 conda 环境,每个项目一个.venv,总磁盘占用比之前一个 Anaconda 目录还小。最后分享一个小技巧:如果你之前像我一样经常怀疑“这个包到底装没装”,可以试试uv tree,它能打印当前环境的完整依赖树,哪个包依赖了哪个包、版本是不是有冲突,一眼就能看明白。这套切换做完之后,我的环境管理省心程度直接上了一个台阶。