☰
从Anaconda到uv:Python包管理工具的迁移实战指南
2026/10/19 12:21:04 网站建设 项目流程

上个月清理笔记本磁盘的时候,我发现一个一直想处理但始终没动手的问题: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 | sh

Windows 可以用 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.py

uv 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,下面的对照表可以帮你快速上手。我不建议把这张表当成速查手册收藏了事,而是建议你实际跑一遍,建立肌肉记忆。

功能condauv
创建虚拟环境conda create -n myenv python=3.12uv venv .venv --python 3.12
激活环境conda activate myenv不需要显式激活,用uv run即可
安装包conda install requestsuv add requests
安装开发依赖conda install pytest -c conda-forgeuv add --dev pytest
删除依赖conda remove requestsuv remove requests
查看环境列表conda env list每个项目一个.venv,不集中列出
导出环境conda env export > env.ymluv lock生成 uv.lock
复现环境conda env create -f env.ymluv 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 hello

uvx会临时创建一个环境、安装 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 之后,流程变得极其统一:

  1. 克隆仓库;
  2. uv sync;
  3. 跑测试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,它能打印当前环境的完整依赖树,哪个包依赖了哪个包、版本是不是有冲突,一眼就能看明白。这套切换做完之后,我的环境管理省心程度直接上了一个台阶。

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

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

立即咨询