说明
uv 是 Astral(Ruff 背后的团队)用 Rust 写的 Python 包管理器,官方口号是 10-100 倍快于 pip。快只是结果,真正值得搞清楚的是:它内部到底怎么工作的。
这篇文章不讲功能清单,只回答一个问题:每敲一条命令,uv 在背后做了哪几步、为什么这么设计。
先给结论:uv 所有命令都建立在两个基础设计上——
- 一个全局仓库:所有下载过的包、构建产物、甚至 Python 解释器本体,全在你电脑上只存一份(
~/.cache/uv) - 链接代替拷贝:任何环境要装包,不复制文件,只创建指向全局仓库的链接
记住这两点,下面每个命令的流程都能看懂。
uv init:创建项目
敲下 uv init my-project,它做四件事:
- 生成一张"项目身份证":
pyproject.toml,里面写着项目叫什么、版本号、要求什么以上的 Python、依赖列表(初始为空)。以后所有工具都靠读这张卡片了解这个项目。 - 钉住 Python 版本:写一个
.python-version文件,比如3.12。这个项目里所有后续操作都用这个版本。 - 搭好代码骨架:建
src/my_project/目录和一个入口函数,并在身份证里登记"装好我之后,命令行里会有个叫my-project的命令可以用"。 - 初始化 git:顺手
git init并写好.gitignore。
值得注意的一点:这时候还没有创建任何虚拟环境。uv 是懒的——环境等到你第一次真正要装包或跑代码时才建,避免为可能永远不用的项目浪费磁盘。
uv add requests:加依赖(最核心的流程)
这一条命令背后是一条五步流水线:
第一步:先把名字记下来
立刻把 requests 写进 pyproject.toml 的依赖列表。此时还不知道该装哪个版本,先占个位。
第二步:算账
requests 自己还要依赖 certifi、urllib3 等一堆包,那些包又有各自的依赖。uv 把整棵依赖树上所有"我要这个范围、你不能超过那个版本"的条件收集齐,然后计算出一套所有包都满意、互相不打架的精确版本。
- 这一步是纯计算。如果条件本身矛盾(A 要 x>=2 而 B 要求 x<2),会直接报错并告诉你谁跟谁冲突
- 算法上有记忆能力:发现过"这两个版本不能共存",下次遇到同样的组合直接跳过,不浪费时间重试
第三步:拿货
对每个定好版本的包,先查全局缓存:
- 有 → 直接复用,一毫秒都不碰网络
- 没有 → 所有缺的包同时发起下载,不是排队一个个来
第四步:记账
把结果写进 uv.lock 锁文件:每个包的精确版本、下载地址、内容哈希值。这份记录覆盖所有操作系统,同事在 Windows 上拿到同一个文件,装出来的版本和你一模一样。哈希值还有个副作用:下载时顺便校验文件没被篡改或损坏。
第五步:接线
如果 .venv 还不存在,此刻创建一个。然后把所有包装进去——但不是拷贝文件,而是在环境里创建指向全局仓库的链接。“Installed 5 packages in 4ms” 说的就是这步:只是建了些目录项而已。
最后回头把第一步占位的名字补全成 requests>=2.33.1。
为什么它快?答案藏在四步里
- 第二步并行下载 → 网络等待时间从"所有包加起来"变成"最慢的那一个"
- 第三步只存一份 → 磁盘不浪费,第二次装直接命中
- 第四步只建链接 → “安装"听起来很重,实际是个轻量操作
uv sync:让环境和账本对齐
适用场景:刚 clone 了别人的项目,或者拉取了同事改过依赖的代码。
逻辑非常简单:
- 读锁文件
uv.lock - 检查当前
.venv里实际装了什么 - 算差集:缺的装上,多的删掉,版本不对的换掉
- 全部从缓存链接,几秒内完成
所以团队协作的固定动作是三连:git pull → uv sync → 干活。不需要知道对方到底加了什么包。
uv run:跑代码
uv run python main.py 表面是运行命令,实际前面多了一道安检:
- 对比
pyproject.toml和uv.lock是否被改动过 - 如果改过 → 先自动执行一次同步,保证环境是最新的
- 在项目的虚拟环境里、用项目钉住的那个 Python 执行你的命令
价值在于:你永远不会跑到一个过期或不一致的环境里。传统流程中"忘了重新装依赖导致报错"这类事故,在这里结构上不可能发生。
uv python install 3.13:安装 Python 本体
uv 连 Python 解释器都当成普通包来管理:
- 下载一份独立的 CPython 构建(官方编译好的、放在独立目录里的完整 Python)
- 存进自己的管理目录,和系统自带的 Python 互不干扰
- 之后
uv python pin 3.13就能让项目切换到它
意义:“机器上没有某个 Python 版本"这件事被消除了——缺什么版本,一条命令就有,不再需要 pyenv。
uv tool install:安装命令行工具
针对 ruff、black 这类"装完是为了敲它的命令"的工具。以一条真实命令为例:
| |
六步:
- 获取源码:clone 这个 git 仓库,把
master分支固定成一个具体的提交号。之后所有操作都基于这次快照,不会中途变卦。 - 现场打包:源码不能直接装。uv 读仓库里的构建配置,调起构建工具,把源码打成一个标准 wheel 包(wheel 就是 Python 生态约定的安装单位,本质是个规范布局的压缩包)。打好的包存进缓存,同一份代码下次不再重复打包。
- 展开完整清单:
[cli,server]表示额外勾选两组可选功能,对应的附加依赖会被加入清单,一起参与第二步那样的算账。 - 重建专属环境:为这个工具单独建一个虚拟环境(已有旧的则整个删掉重建——
--force就是允许这种覆盖)。 - 装入:所有包从缓存链接进这个专属环境。
- 接出命令:读包元数据里声明的入口,把
powercontext命令的快捷方式放进~/.local/bin。因为这个目录在 PATH 里,你在任何位置都能直接敲这个命令。
两个推论值得记住:
- 每个工具住在自己独立的房子里,ruff 依赖 A 库 9.0、black 依赖 A 库 10.0 也相安无事
- 装完后命令指向的是当时的代码快照。上游更新后重跑安装即可刷新;如果缓存没生效,用
--reinstall强制重来
uvx ruff check:临时用一次
上面 tool install 是"长期雇佣”,uvx 是"日结工”:
- 环境不放在工具目录,而是丢进缓存里,属于临时工位
- 用完即走,不在系统里留全局命令
- 第二次跑同一个工具时,临时环境还在缓存里,直接复用,依然很快
适合偶尔用一次的工具,不值得为它在系统里常驻一个环境。
总览
| 命令 | 一句话机制 |
|---|---|
uv init | 写身份证 + 钉 Python 版本 + 搭骨架,环境先不建 |
uv add | 记名字 → 算账 → 拿货 → 记账 → 接线 |
uv sync | 环境和锁文件求差集,缺啥补啥 |
uv run | 先安检(环境是否最新),再在正确环境里执行 |
uv python install | 把解释器当包管理,下载独立构建版 |
uv tool install | 取码 → 打包 → 独立环境 → 接出命令 |
uvx | 同上但用完即弃 |
动手验证:亲眼看看"接线"
不信"安装只是建链接"?两条命令验证:
| |
两边 inode 数字相同 = 磁盘上是同一份数据,.venv 里真的只是一个入口标记。
深入方向
本文刻意停在"流程讲清楚"这一层。再往深处,有三个方向值得单独成文:
- 依赖解析算法:uv 用的是 PubGrub(和 Cargo、Dart 同款),核心思想是"冲突学习"——发现某两个版本不能共存就永久记住,同类错误不再重试
- 缓存目录结构:
archive-v0、git-v0等分桶设计,以及硬链接失效的场景(跨磁盘、Docker overlayfs) - 解析器的并发模型:解题线程和网络下载线程如何分工互不拖累
参考资料: