新机器上克隆一个 Python 项目,README 第一条通常是 python -m venv .venv && pip install -r requirements.txt。网络一般时要等两三分钟,网络差时直接超时;更麻烦的是 requirements.txt 里没有锁,今天装出来的环境和上周不是同一套。CI 每次重建缓存,账单上大部分时间花在装依赖。
uv 想解决的就是这一整段流程:解析、下载、建环境、锁版本、装解释器。它用 Rust 写成,最初以「pip 的替代品」被人知道,但真正值得用的是它的项目管理层。
它替换的其实是四样东西#
把 uv 当成「快一点的 pip」会浪费它一半能力。按职责拆开看:
| 职责 | 传统做法 | uv 的做法 |
|---|---|---|
| 依赖解析与下载 | pip |
uv pip / uv add |
| 虚拟环境 | venv、virtualenv |
uv venv,uv run 自动创建 |
| 锁文件 | pip-tools、poetry.lock |
uv.lock |
| 解释器版本 | pyenv、系统包 |
uv python install |
三层接口是分开的:uv pip install 是 pip 兼容层,用来迁移老项目;uv add / uv sync 是项目管理层,读写 pyproject.toml 和 uv.lock;uv python 管解释器。新项目直接进第二层,别停在第一层。
一个最小工作流#
下面这套命令跑完,项目就有了可复现的环境。uv 的安装方式有独立安装脚本、pipx、包管理器几种,具体命令以官方文档为准。
uv init demo && cd demo # 生成 pyproject.toml 和 .python-version
uv add httpx # 写依赖,同时更新 uv.lock
uv add --dev pytest # 开发依赖进 dependency-groups,不污染运行时
uv sync # 按锁文件精确还原环境
uv run pytest -q # 在项目环境里执行,不需要手动 activate
三个约定值得写进团队规范:
uv.lock提交进仓库,它是跨平台的,带环境标记,不要在 CI 里重新生成;- CI 里用
uv sync --frozen,锁文件与pyproject.toml不一致时直接失败,而不是悄悄改写; - 入口统一用
uv run,避免「我本地是好的」这类环境差异。
uv run 会发现 .venv 不存在就按锁文件建一个。这一步省掉的是新人上手时最常见的失败:忘了 activate,或者 activate 了另一个项目的环境。
锁文件里到底记了什么#
uv.lock 记的不只是版本号,还有每个包的来源、wheel 哈希和依赖边。它让「在另一台机器上装出完全一样的环境」变成可验证的事,而不靠运气。
看锁文件时关注两处:一是 resolution-markers,它解释了同一个包为什么在不同 Python 版本下选了不同版本;二是缺哈希的条目,那通常来自实验性操作,不该出现在主干分支。这也解释了一个常见疑问:锁文件冲突时不要手改。它是解析结果,手改等于伪造结论,下次 uv lock 会把改动冲掉。冲突时正确的动作是回到 pyproject.toml 调约束,再重新解析。
依赖分组也在这里生效。uv add --group docs sphinx 只影响安装范围,不改变解析结果;但平台差异要走环境标记,比如 sys_platform == "win32",不要拆成两套依赖表,否则锁文件会分叉成两份事实。
快在哪里,以及怎么自己量#
「10 倍」是标题里的量级说法,不是可以引用的数字。真实差距取决于包数量、缓存状态和网络,所以更靠谱的做法是自己测一遍。
uv 的速度来自几处叠加:
- 解析器用 Rust 实现,依赖图求解比 pip 的递归回溯快;
- 全局缓存加硬链接,同一个 wheel 在磁盘上只存一份,多个 venv 共享,装第二个环境几乎不下载;
- 并行下载、并行解包,索引元数据本地缓存;
- 解释器用 python-build-standalone 的预编译产物,不用现场编译。
测量时要控制变量,同一台机器、同一网络、同一组包,各跑三次取中位数:
# 冷缓存:先清掉 uv 缓存再计时
uv cache clean && time uv sync
# 对照:全新 venv 里用 pip 装同一组依赖
python -m venv /tmp/pipenv
time /tmp/pipenv/bin/pip install -r requirements.txt
冷缓存下差距通常不大,因为瓶颈在下载带宽;热缓存、多环境、CI 复用缓存时差距最明显。小项目只有五六个依赖,差距会缩到很小,这时候换工具的理由是锁文件和统一入口,不是速度。
测完把结论落到 README 或 CI 注释里:包数量、冷热缓存、耗时中位数、机器规格。以后有人再说「快十倍」,至少有一组自己环境下的数字可以对照,而不是转述别人的截图。
报「快 N 倍」时一定注明是冷缓存还是热缓存、包数量多少、网络条件如何。脱离这三项的倍数没有意义。
[!WARNING] 在已有 CI 上换工具链,先在一台干净的 runner 上跑通「装依赖 + 跑测试」,再改主分支的流水线。缓存路径换了、镜像源没配,失败通常出现在第二步。
坑与边界#
先用一段话把边界说在前面:uv 能替掉的是「Python 包的安装与解析」,替不掉系统库、审计流程和团队习惯。下面两节分别列具体的坑和不该换的场景。
容易踩的坑#
- 私有源配置:用
[[tool.uv.index]]或UV_INDEX_URL环境变量,token 走环境变量,不要写进pyproject.toml提交。 - 混用两层接口:
uv pip install装进当前环境但不会更新uv.lock,锁文件和实际环境会分叉。要么全用uv add,要么明确知道自己在做临时实验。 - 平台相关依赖:
uv.lock是通用的,但带平台标记的包在目标机上仍要能装;只发 sdist 的包需要编译工具链,速度优势会打折。 - 内网解释器下载:
uv python install要访问外部源,公司网络可能需要镜像或预置解释器。 - 老项目迁移:先用
uv pip compile requirements.in -o requirements.txt拿到确定的版本,再逐步把依赖搬进pyproject.toml,别一次性重写。
什么情况下先别换#
uv 不是所有项目的正确答案。遇到下面几种情况,留在原工具链更省事:
- 组织的制品审计、镜像、漏洞扫描都绑死在
requirements.txt上,换工具意味着改流程; - 依赖 conda 生态(CUDA、GDAL 这类非 Python 二进制依赖),uv 管不到系统库;
- 项目深度使用 Poetry 的扩展机制或私有 build backend,迁移成本高于收益;
- 团队里没人愿意维护新工具,那锁文件迟早会失修。
结论#
- 新项目直接上
uv init+uv add+uv sync,把uv.lock当代码一样 review。 - CI 用
--frozen,本地用uv run,两条规则能消掉大部分环境不一致问题。 - 想验证速度就自己测:冷缓存、热缓存分开跑,记录包数量和耗时,别引用别人的倍数。
- 老项目先走
uv pip兼容层,验证可用后再搬pyproject.toml。 - 依赖系统级库或强绑定审计流程的项目,可以晚点再换。
完