我花 1 小时迁移 5 个项目到 UV,CI 快了 5 倍
版权声明
我们非常重视原创文章,为尊重知识产权并避免潜在的版权问题,我们在此提供文章的摘要供您初步了解。如果您想要查阅更为详尽的内容,访问作者的公众号页面获取完整文章。
TL;DR
UV 不是更快的 pip——它把 pip/venv/pyenv/pipx/poetry 六个工具焊进一个 50MB 的 Rust 二进制文件,实现 10-100x 加速 核心机制不是"优化"而是架构重构:PubGrub 冲突学习 + 硬链接全局缓存 + 异步并行 I/O 流水线 大多数项目 1 小时内完成迁移—— uv init→uv add -r requirements.txt→uv sync,CI 时间直接砍 5-10 分钟
01你浪费在 pip 上的时间,够学一门新语言了
来,先算一笔账。
一个 Python 项目从零到能跑:python -m venv .venv(2 秒),pip install 十几个包(2 分钟),跑起来发现 numpy 版本不对,删 .venv,重来一遍(又 2 分钟)。这只是项目初始化。
日常开发中呢?改一个依赖版本,pip install --upgrade 跑 30 秒。切一个分支,依赖变了,再跑 30 秒。CI 里每次 pip install -r requirements.txt 烧掉 3-5 分钟。如果你同时维护 5 个项目,每个项目的 requirements.txt 都像一份历史遗产——你分不清哪些是直接依赖,哪些是间接依赖,哪些已经可以删了。
我在过去两年见过至少 10 个团队做同一件事:把 requirements.txt 拆成 requirements-base.txt、requirements-dev.txt、requirements-prod.txt,然后在一个深夜对着 pip freeze 的输出发呆——"这 37 个包里,到底哪些是我主动装的?"
DevOps 圈花了十年从"手动 scp + ssh + 重启"演进到"git push → CI/CD 自动部署"。Python 的包管理呢?二十年了,还在手动挡。
直到 UV 出现。
这不是"pip 的竞品"。这是把你工具箱里 6 个零散工具(pip、pip-tools、venv、pipx、pyenv、twine)全部拆开,取出各自的核心逻辑,用 Rust 重写,然后焊进一个 50MB 的二进制文件。
你换的不是一个工具——是一整套工作流。
数字不说谎。安装 Django 全家桶:pip 冷缓存 45 秒,UV 3 秒。温缓存更夸张——pip 12 秒,UV 0.5 秒。创建虚拟环境:python -m venv 接近 2 秒,uv venv 35 毫秒。56 倍差距。
我第一次意识到 pip 有多慢,是去年给一个 Django 博客项目搭 CI。每次 push 完要等 6 分钟——其中 4 分半在跑 pip install -r requirements.txt。我养成了一个习惯:push 完就去接水。回来大概率还在转。后来项目从 1 个变成 5 个,这个"接水时间"乘了 5。
02一个二进制文件,凭什么比整个 Python 生态快 10 倍?
先破除一个迷思:UV 快不是因为"pip 写得烂"。pip 慢的原因有三个,全是结构性限制,不是实现缺点。
第一,pip 是 Python 程序
pip 每次启动都要走一遍 CPython 解释器的加载流程——解析、编译字节码、初始化运行时。这个过程差不多 300 毫秒,在"装 50 个包"的场景里可以忽略。但在"检查这个包有没有新版本"(uv 可以在 0.33 毫秒内解析完 2 个包)的场景里,这 300 毫秒就是 1000 倍的差距。UV 是编译好的原生二进制,没有这个"起跑税"。
第二,pip 不能真正并行
CPython 有 GIL(全局解释器锁),pip 的下载、解压、安装三个环节实际是串行的——下完一个包,解压,安装,再下第二个。UV 用 Rust 的 tokio 异步运行时 + reqwest HTTP 客户端,实现了真正的多核并行:一边下载包 A,一边解压包 B,一边把包 C 装进虚拟环境。这是架构层面的差异,不是"优化一下"能追上的。
第三,pip 每次都在"搬家"
pip 安装一个包时,会把文件从 PyPI 下载到临时目录,再复制到虚拟环境的 site-packages/。你的硬盘上可能有 5 个 Django 项目的虚拟环境,每个里面都有一份完全相同的 Django 源码文件。UV 的做法完全不同:全局缓存(~/.cache/uv)保存了每份包的唯一样本,项目虚拟环境通过硬链接(hard link)指向这份样本。硬链接不复制数据——它只是给同一份磁盘数据多起一个名字。温缓存下的"安装"实质上是"创建文件系统指针",毫秒级完成。
用个比喻帮你记住这三条:pip 像一个人去仓库取货、拆箱、摆到货架,一件一件来。UV 像自动化分拣中心——货物(包)到达即识别(校验哈希)、自动入库(全局缓存)、配送时通过传送带(硬链接)秒级到货。
你可能会问:这不就是 Node.js 的 pnpm 做的那套吗?对。UV 的设计理念直接借鉴了 pnpm 的 content-addressable store。在这个设计下,50 个 Django 项目共享同一份 Django 的磁盘 inode——这是真正的一劳永逸。
03PubGrub:UV 的大脑如何解开依赖死结
到这里,你知道了 UV 的"肌肉"(硬链接缓存 + 并行 I/O)为什么快。但 UV 还有一个更底层的优势,藏在依赖解析这个环节。
依赖解析,通俗讲就是:给了你 pyproject.toml 里列的 A、B、C 三个包,每个包又要求特定版本的其他包(比如 flask 需要 werkzeug>=2.0,<3.0),UV 需要找到一组"所有人都不打架"的版本组合。
这是一个搜索问题。pip 的策略可以粗略理解为:从最新版本开始试,碰到冲突就往回退一步,换一个版本再试。这叫递归回溯。在最坏情况下,pip 需要试完所有可能的版本组合——如果 10 个包各有 5 个可用版本,搜索空间就是 5^10,差不多 1000 万种组合。
UV 用的是 PubGrub 算法(Rust 实现的 pubgrub-rs),这不是简单的"聪明一点的搜索"。PubGrub 的核心机制叫做冲突驱动子句学习(CDCL)——每当发现一个冲突(比如 a==2.0 → c>=3.0 和 b==1.0 → c<3.0 冲突了),UV 会"学习"一条规则:"a==2.0 和 b==1.0 不能同时存在"。然后回溯到冲突发生之前,换一条路走,并且永远不再尝试这个组合。
我用数独打个比方。pip 的解法是"先填一个数,走不通擦掉重填"。UV 的解法是"每发现一个冲突,就在格子旁边写'这两个格子不能同时是 3',以后绕开"。在数独里,前者的回退次数可能上千,后者可能只需要十几次。
这就是为什么 UV 解析 Apache Airflow 的依赖树只用了 0.21 秒,而 pip-tools 用了 13.89 秒。65 倍的差距,来自搜索策略的降维打击。
???? 深入:PubGrub 解析器的内部机制
这部分适合有编译系统或算法背景的读者。如果你只关心怎么从 pip 迁移到 UV,可以直接跳到下一章。
PubGrub 解析器在 UV 中的实现有 5 层优先级调度:Root(虚拟根包,永远先处理)→ DirectUrl(Git/本地路径引用)→ Singleton(单一版本约束,如 ==1.0)→ ConflictEarly/ConflictLate(被冲突频次动态调整优先级)→ Unspecified(普通包,按发现顺序 FIFO)。
这个优先级系统解决了一个实际问题:被反复冲突的包(ConflictEarly)会被提升优先级提前处理,让冲突尽早暴露。反之,频繁"害别人冲突"的包(ConflictLate)被降级,避免它过早锁死版本选择。这是一个自适应的调度系统——解析器在运行中学习哪些包是"刺头"。
UV 对 PubGrub 最关键的扩展是 Forking Resolver(分叉解析器)。Python 依赖有个独有的麻烦:同一个包的依赖可能因平台而异。比如 numpy>=2.0 ; python_version >= "3.11" 和 numpy>=1.16,<2.0 ; python_version < "3.11",这两条要求在同一个锁文件中无法同时满足。UV 的解法是:检测到不兼容的平台 marker 时,把解析分叉为两个独立分支(一个给 python >= 3.11,一个给 python < 3.11),各自独立解析,最后合并写回同一个 uv.lock 文件。
锁文件中的 resolution-markers 字段记录了这些分叉——uv sync 时按当前平台的 marker 选择对应分叉,实现了"一个锁文件,全平台可重现"。
04从 pip 到 UV:一天迁移,十年收益
好,原理讲够了。你可能最关心的是这一步——我现在就有一个项目,怎么从一个 pip install -r requirements.txt 的世界搬过去?
别担心。迁移不是"把房子拆了重建",是"一间一间装修"。你带着旧家具住着,边用边搬。
第一步:装 UV
# macOS/Linux curl -LsSf https://astral.sh/uv/install.sh | sh # 或者用 pip 安装(对,用 pip 装 uv,然后就不再需要 pip 了) pip install uv # 验证 uv --version # uv 0.11.14 (这是 2026 年 5 月的最新版)
第二步:初始化项目
# 在你的现有项目根目录下 cd my-project/ # UV 会检测已存在的文件(requirements.txt / pyproject.toml),不会覆盖 uv init # 如果已经有 requirements.txt,直接导入依赖 uv add -r requirements.txt # 这一步会:读取 requirements.txt → 解析每个包 → 写入 pyproject.toml → 生成 uv.lock
第三步:同步环境
# 创建虚拟环境 + 安装所有依赖,一次搞定 uv sync # 冷缓存首次跑:下载 + 安装,看网络速度 # 之后每次跑:0.5 秒内完成(硬链接 + 缓存命中) # 启动你的项目 uv run python main.py
就这样。三行命令,你的项目从"pip + venv + requirements.txt"切到了"uv + uv.lock + pyproject.toml"。
一个真实的例子:我在一个中等规模的 FastAPI 项目上做了迁移测试——57 个依赖包,4 个 requirements-*.txt 文件。uv add -r requirements-base.txt -r requirements-dev.txt 一次性全部导入,uv sync 冷缓存下 8 秒完成(之前 pip install -r 需要 45 秒)。温缓存下 uv sync 0.8 秒。重复跑 CI 管道的时间从 9 分钟降到了 4 分半。
我自己的数据更狠一点。把「数据STUDIO」这个号的选题库管理脚本从 pip 迁到 UV——23 个依赖,迁移本身用了不到 20 分钟(主要是把 requirements.txt 里的包名一个一个确认版本号),uv sync 冷缓存 4 秒,温缓存直接 0.6 秒。最爽的是 GitHub Actions:之前每次 CI 跑 pip install 烧 3 分钟,现在 uv sync 稳定在 12 秒以内。3 分钟变 12 秒,这不是"快了一点",是"我不用再盯着 CI 进度条发呆了"。
第四步:更新 CI 配置
GitHub Actions 的改法很简单。这是之前:
- uses: actions/setup-python@v5 with: python-version: "3.12" - run: pip install -r requirements.txt - run: python -m pytest
改成这样:
- uses: astral-sh/setup-uv@v6 with: python-version: "3.12" - run: uv sync - run: uv run pytest
astral-sh/setup-uv 这个 Action 是 UV 官方维护的——它会自动缓存全局依赖目录,缓存命中率极高。算上缓存,uv sync 在 CI 里通常不超过 5 秒。
pip → uv 命令速查表
一行规律:以前散落在 pip、pip-tools、venv、pipx、pyenv 里的操作,现在全部以 uv 开头。
05跨平台锁文件:macOS 上能跑,Linux CI 里为什么崩了?
迁移做完后,你拿到的最有价值的东西不是速度,是一个叫 uv.lock 的文件。
用过 pip freeze 的人都知道它的毛病:它只是把你当前环境里装了什么"拍一张快照",不记录依赖树结构,不区分直接依赖和间接依赖,更没有跨平台的版本区分。你在 macOS 上跑 pip freeze > requirements.txt 生成的文件,到 Linux CI 上跑 pip install -r requirements.txt,崩了——因为某个包在 macOS 和 Linux 上的子依赖版本不同。
UV 的锁文件解决的就是这个问题。
uv.lock 是一个跨平台的"通用锁文件"(universal lockfile)。当你跑 uv lock 时,UV 的 Forking Resolver 检测到有不同平台的 marker 要求(比如 subprocess32 ; python_version < '3.0' ; sys_platform == 'win32'),会自动把依赖解析分叉为多个独立路径,最后序列化到同一个 uv.lock 文件里。
你在 macOS 上写代码,uv lock 一把锁;CI 在 Linux 上跑 uv sync,从同一份 uv.lock 里读到自己平台的版本——每个人拿到的依赖版本完全一致。
这就是 Rust 的 Cargo 做了十年的事。Python 终于有了自己的 Cargo.lock。
一个具体的场景:你的 Django 项目依赖 数据STUDIOPillow(图片处理),而 Pillow 在 macOS 和 Linux 上需要的底层 C 库版本不同。用 requirements.txt 时,你需要在文件里写 Pillow ; sys_platform == 'darwin' 这类条件注释——手动维护,容易出错。用 uv.lock,这一切自动处
文章来源:
点击领取《Python学习手册》,后台回复「福利」获取。『数据STUDIO』专注于数据科学原创文章分享,内容以 Python 为核心语言,涵盖机器学习、数据分析、可视化、MySQL等领域干货知识总结及实战项目。
还在用多套工具管项目?
一个平台搞定产品、项目、质量与效能,告别整合之苦,实现全流程闭环。
白皮书上线