Python 的环境管理一直是新手的第一个坎:装了个包,过两天发现另一个项目跑不起来;换台机器,pip install -r requirements.txt 装出来的东西跟原来那台就是不一样。

一年多前写 Python 打包那篇时,我说 uv 还只是个装包工具。这一年它把我这边的依赖管理整个换掉了,所以专门写一篇说清楚换的原因和换的过程。

这套痛点的根源不复杂:requirements.txt 记录的是”要什么”,不记录”装到了什么”。没有锁文件,同一份依赖清单在不同时间装出来就是不同的东西。

老方案的几种补丁

这些年大家打的补丁大致三类。

pip-tools 的思路是编译:你写 requirements.in(只写直接依赖),它算出完整依赖树,生成锁好的 requirements.txt。这个思路是对的,缺点是要手动跑两步,而且虚拟环境还得自己管。

poetry / pdm 把环境、依赖、打包、发布揉进一个工具,配置集中在 pyproject.toml 里,锁文件自动维护。功能全,代价是它要接管的事情多,跟 pip 生态混用的时候偶尔打架。

conda 解决的是另一层问题——它管的不只是 Python 包,还有 Python 本身的版本和那些非 Python 的二进制依赖。做科学计算的人离不开它,原因是 numpy、scipy 这类库背后的编译依赖,pip 处理起来确实费劲。

这三类方案都能用,共同的问题是:慢。解析依赖、建环境、装包,动不动几十秒起步。

uv 带来的变化

uv 是用 Rust 写的,主打的就是速度。我主观感受是建环境和装包基本到了”感觉不到在等”的程度。

它有两个使用层次,我觉得这个设计很聪明:

兼容 pip 的接口。 uv pip install 这类命令跟 pip 的用法几乎一样,所以迁移成本极低——你不用把项目结构改一遍,换个命令就能感受到提速。

自己的项目管理。 uv init、uv add、uv sync,配 pyproject.toml 和 uv.lock。这一步是它跟 poetry 抢地盘的地方。

我现在的用法是:老项目先只换 pip 那一层,图个快;新项目直接用它的项目管理,一步到位。

让我最想推荐的一个功能

单文件脚本的内联依赖。PEP 723 定义了一种写法,可以在 .py 文件头里声明这个脚本需要什么包:

# /// script
# requires-python = ">=3.11"
# dependencies = ["requests", "rich"]
# ///

然后 uv run script.py,它会自动建一个临时环境、装好依赖、跑起来。你不用为了一次性的脚本去建虚拟环境、写 requirements、再删掉。

这个功能对我这种经常写一次性脚本的人,价值比”快”还大。以前写个小工具,光是环境准备就够让人放弃的。

迁移时要注意的事

锁文件一定要提交。 uv.lock 进仓库,别人才能装出一模一样的环境。这条是底线,如果只能记住一件事,就记这个。

CI 里要改。 原来 pip install -r requirements.txt,现在是 uv sync 或者 uv pip install -r ...。别只改本地不改 CI,那是最常见的翻车方式。

别混着用。 同一个项目里 poetry 和 uv 各管一半,迟早出问题。选一个,全套用它。

私有源的配置。 用内部 PyPI 的话,注意新工具读的是哪份配置,有时候它会去找 pip 的老配置文件,有时候要单独配。

虚拟环境还要不要手动管

传统流程是:建虚拟环境、激活、装包、用完退出。这套动作每天要做很多次,而且经常忘——忘了激活就在全局环境里装包,过一阵子系统 Python 就乱成一锅粥。

新工具普遍把这一层藏起来了。uv run 自动用项目环境跑命令,uv sync 保证环境跟锁文件一致。你不需要知道环境目录在哪,也不用每次记得激活。

这个变化看着小,实际影响挺大:它把”记得做正确的事”变成了”默认就是对的”。以前团队里总得有人写一段 README 强调”请务必先激活虚拟环境”,现在这段可以删掉。

唯一需要注意的场景是跟老工具混用的时候,搞不清当前生效的到底是哪个环境。which python(Windows 上是 where python)这类检查命令该用还是得用,别嫌麻烦。

什么时候还是绕不开 conda

前面说的都是纯 Python 项目。一旦涉及科学计算,情况会复杂一点。

numpy、scipy、pytorch 这类库背后有大量非 Python 的二进制依赖:BLAS、CUDA、编译工具链。pip 装它们靠的是预编译 wheel,大多数情况下没问题,但一遇到”某个平台的 wheel 不存在”或者”需要指定版本的 CUDA”,就会很痛苦。

conda 的优势正好在这里:它能管这些非 Python 依赖,还能管 Python 本身的版本。所以在 GPU 环境、老平台、需要精确控制编译选项的场景,它依然是首选。

我自己的分工是:写工具、写服务用 uv;做模型训练和科学计算用 conda。两边的目录分开、锁文件分开,别互相污染。混用最典型的翻车是 conda 环境里再 pip install 一个重名的包,之后环境就说不清是谁管的了。

迁移的实际顺序

老项目想换过来,我建议按这个顺序走,一次只做一步,每步都能独立回退:

  1. 先在本地只换安装命令(uv pip install),不动项目结构,先感受速度
  2. 生成锁文件、提交进仓库,让 CI 也用上
  3. 再把依赖声明搬进 pyproject.toml,改用 uv sync
  4. 最后才动打包和发布

我见过有人一口气全改,结果构建挂了三天,还不知道是哪一步引入的问题。分步做的代价是多花一点时间,收益是出问题的时候永远知道该回退到哪。

工具之争不重要

工具之争其实不重要。我见过用 pip 加手写锁文件也管得很好的团队,也见过工具链很时髦但环境天天崩的项目。

真正决定体验的只有两条:有没有锁文件,以及环境能不能一条命令重建。做到这两条,用什么工具都行;做不到,用什么工具都难受。

uv 值得换的理由,是它把这两条变成默认行为——你不需要额外下决心去做,它本身就那么工作。这种设计比”功能多”重要得多。