pyproject.toml · uv.lock · .venv · 全局缓存 · 硬链接 · uv run · Python 版本

uv:一个命令管好 Python 版本、依赖和虚拟环境

Python 的 import 只认一个装包的目录,所以每个项目都得有自己的一份目录,叫虚拟环境。uv 的精华:你在 pyproject.toml 里写想要什么,uv 把整棵依赖树钉死在 uv.lock 里,再按锁文件把 .venv “算”出来;包从全局缓存硬链接过去,重建一次只要几十毫秒,所以 .venv 随时可以删掉重建,它和锁文件永远对得上。不懂这一点的典型用法,是钻进 .venv 手动 pip install:环境变了,锁文件没变,同事那里就跑不起来,下一次 uv sync 还会把它删掉。

全景:一个 FastAPI 请求的一生 · 第 1 站地图:站 01 · uv 管环境和依赖(对照 Maven)以 uv 0.13.7、Python 3.13 为例

①为什么学:curl 打进来之前,得先有一个对的进程在等着

在全景第 1 站,这一步要解决的是:20:15 你敲下 curl /gc/summary 之前,8000 端口上得有一个 uvicorn 进程。它要跑在 Python 3.13 上,带着版本对得上的 28 个包,其中 uvloop、pydantic-core 这几个是编译好的二进制,要和机器的 CPU、系统对得上。而且你的笔记本、同事的电脑、CI、值班机,装出来的东西要一模一样。

你(或 systemd) uv run uvicorn main:app 第 1 站 · uv 找到 Python 3.13,按 uv.lock 装好 28 个包 到 .venv,再在里面起 uvicorn 进程 第 2 站 · uvicorn 在 8000 端口等 curl 第 3~10 站… 同一份 pyproject.toml + uv.lock 提交在 git 里,30 个包的版本和 sha256 都钉死 uv sync 你的 Mac(arm64) macOS 版 wheel 版本、哈希和 lock 一致 同事的 Windows lock 写着 uvloop 非 win32 才装 按标记跳过,多装 colorama CI(Linux x86_64) uv sync --locked lock 过期就直接失败 值班机(Linux aarch64) 本页实测就在这种机器上 manylinux aarch64 版 wheel
第 1 站要交出的东西:一个跑在对的 Python 上、装着对的包的进程。Java 里这件事由 JDK 加 Maven 管:mvn 按 pom.xml 去 ~/.m2 找 jar,启动时拼成 classpath。Python 这边换成了 uv。

②老办法:系统 Python 加 pip,能跑,但换台机器就不一样

Java 程序员第一次写 Python,最自然的做法是用系统自带的 python3,缺什么 pip install 什么。在本机上第一步就碰壁了:

本机系统 Python,直接跑值班助手的代码实测
$ python3 --version
Python 3.10.12                       # Ubuntu 22.04 自带
$ python3 -c 'import asyncio; asyncio.timeout'
AttributeError: module 'asyncio' has no attribute 'timeout'   # 3.11 才有,main.py 的超时就靠它
$ python3 -c 'import fastapi'
ModuleNotFoundError: No module named 'fastapi'

Python 版本 pip 管不了,得另外装(pyenv、系统包、官网安装包)。新一点的 Debian、Ubuntu 还会直接拒绝往系统 Python 里 pip install,报 externally-managed-environment(PEP 668),逼你用虚拟环境。

于是老办法升级成 python -m venv .venv + pip install + pip freeze > requirements.txt。要看清它疼在哪,先对照 Maven 为什么没有这个问题:

Mavenjar 只存一份,classpath 是启动参数

~/.m2/repository(每个版本一份) jackson-2.17.jar jackson-2.15.jar spring-web.jar 两个版本并排放着,互不打架 订单服务 pom.xml -cp jackson-2.17… 支付服务 pom.xml -cp jackson-2.15… 启动时指过去,不复制
  • 一台机器上,不同项目各用各的版本
  • 项目目录里没有依赖的拷贝

pip + venvimport 只认 site-packages,每个项目拷一份

PyPI(≈ Maven Central) 下载 .whl(≈ jar) 每次解压、复制一遍 gc-helper/.venv site-packages/ fastapi/ (0.162) pydantic/ … 28 个 72 MB · 7.2 s 另一个项目/.venv site-packages/ fastapi/ (0.110) pydantic/ … 又一份 再来一遍
  • Python 版本本身不归 pip 管
  • pip freeze 只记下这台机器此刻装了什么:没有哈希,也分不清哪些是你要的、哪些是被带进来的
  • 加了包忘记 freeze,同事那里就跑不起来

差距在一处:Java 的 classpath 是启动参数,Python 的包要装进一个目录。所以 Python 必须给每个项目一份目录,问题只剩“这份目录怎么又快又准地造出来”。造得准,靠锁文件;造得快,靠缓存加链接。uv 两样都做了。

实测数字的前提:本机 2 核 ARM 云主机,Python 3.13.18,pip 26.2.2,同样装 fastapi、uvicorn[standard]、httpx、pytest。pip 缓存冷时一共 9.3 s,缓存热时建环境 2.5 s 加安装 4.7 s。

③选谁:pip-tools、Poetry、Conda、uv

这几样都能做到“锁住版本、每个项目一个环境”,差在管多宽、多快、跟不跟标准走。

pip + venv + pip-toolsPoetryCondauv
管 Python 版本不管不管,要配 pyenv管管,按需自动下载
锁文件pip-compile 生成带哈希的 requirements.txtpoetry.lock要另装 conda-lockuv.lock,一份覆盖所有平台
依赖写在哪requirements.inpyproject.toml(2.0 起支持标准的 [project] 表)environment.ymlpyproject.toml 标准 [project] 表
能装非 Python 的东西不能不能能:CUDA、MKL、C 库不能
速度慢中慢快:Rust 写的,全局缓存加硬链接
一个工具包办三个工具拼起来依赖、打包、发布环境和包Python 版本、环境、锁、运行、打包、临时跑工具(uvx)

选择规则:新的 Python 服务、脚本、Agent 项目用 uv;要装 CUDA 这类非 Python 二进制、做数据科学的用 Conda;已经在用 Poetry 的老项目不用急着迁。值班助手是一个普通的 Web 服务,没有 GPU 依赖。

本页学 uv:它把 Maven(依赖)、SDKMAN(JDK 版本)和 mvn exec(运行)三样合成了一个命令。

④原理:三个文件、两个仓库,一条单向的路

uv 的命令很多,背后只有一条路:你写的意图 → 钉死的结果 → 本机的环境。看懂这条路,后面的命令、坑和规范都能推出来。

先知道这些四个 Python 基本概念,各用一句 Java 话对上

import 怎么找包

按 sys.path 列表的顺序,找名叫 fastapi 的目录,第一个找到的算数。约等于 classpath,但同一个目录里同名包只能有一个版本。

wheel(.whl)

预先打好的 zip 包,约等于 jar。文件名带平台标签,比如 cp313-manylinux_aarch64,因为里面可能有编译好的 C 或 Rust 代码。

虚拟环境(venv)

就是一个目录:bin/python 链接到真正的解释器,pyvenv.cfg 告诉解释器“我是一个环境”,包装在 lib/python3.13/site-packages。

pyproject.toml

Python 项目的标准描述文件(PEP 621),约等于 pom.xml:项目名、要求的 Python 版本、依赖列表。

核心机制:意图 → 结果 → 环境

pyproject.toml requires-python = ">=3.13" "fastapi>=0.162.2" "httpx>=0.28.1" "uvicorn[standard]>=0.54.0" 你写:意图(范围) 提交到 git uv lock 所有约束 同时满足 uv.lock 30 个包,每个钉死一个版本 383 个 sha256(每个平台的 wheel) uvloop: sys_platform != 'win32' colorama: sys_platform == 'win32' uv 算:结果(精确,全平台) 提交到 git uv sync 按本机挑 多删缺补 .venv/ bin/python → 3.13.18 pyvenv.cfg site-packages/ 28 个包 本机的环境:可删可重建 不进 git PyPI 或公司私服 ≈ Maven Central / Nexus .whl 文件,带平台标签 ~/.local/share/uv/python cpython-3.13.18-linux-aarch64 .python-version 里写 3.13,没有就下载 ≈ SDKMAN 装的 JDK ~/.cache/uv(全局缓存) 每个 wheel 只下载、解压一次 ≈ ~/.m2,所有项目共用 bin/python 是链接 硬链接,不复制 缓存里没有才下载(实测 28 个包 1.7 s)
路是单向的:只能从左往右算,不能从右往左写。uv add 改左边,uv lock 算中间,uv sync 造右边;uv run 每次启动前把这三步都检查一遍,对得上就直接跑。
  1. 1两个文件分工。pyproject.toml 写范围,比如 fastapi>=0.162.2,表示“我能接受哪些版本”;uv.lock 写结果,表示“这一次实际用哪个版本、文件的哈希是多少”。两个都提交到 git,就和提交 pom.xml 一样。
  2. 2锁文件覆盖所有平台。uv 解析时不只为这台机器算,而是为 Linux、macOS、Windows 和 requires-python 允许的所有 Python 版本一起算,用标记(marker)写明哪个包在哪个平台才要。本机实测的 uv.lock 里有 20 个 Windows wheel 和 56 个 macOS wheel 的哈希,虽然这台是 Linux。
  3. 3环境是算出来的。uv sync 默认是“精确同步”:lock 里没有的包,环境里有就删掉。所以 .venv 只是锁文件在这台机器上的投影,可以随手删,几十毫秒就能重建。
  4. 4uv run 自带检查。它先确认 lock 和 pyproject 对得上,再确认 .venv 和 lock 对得上,最后才在 .venv 里起子进程。什么都没变时,这一套检查本机实测 17 ms。

为什么这样设计三个约束,逼出三个选择

约束 1:import 只认目录,每个项目必须有一份自己的包目录。复制太慢,那就只复制一次。

被放弃:每个项目解压一份pip 的做法

fastapi.whl 项目 A一份完整拷贝 项目 B又一份 重建 A再解压一次

每次都要解压和写盘,环境越多越慢、越占地方。

选中:缓存解压一次,项目里只建链接uv 的做法

~/.cache/uv/archive-v0/…/fastapi/routing.py .venv/lib/…/site-packages/fastapi/routing.py inode 1075910磁盘上一份,链接数 2 两个名字,同一份数据 实测

建链接只改目录项,不读写文件内容,所以几乎不花时间。Linux 上默认硬链接,macOS 上默认用写时复制的克隆(按文档)。

建一个新环境要多久(缓存里已有这些包,除非注明) 实测 · 2 核 ARM · 28~29 个包 pip:venv + pip install uv sync,缓存冷(含下载) uv sync --link-mode copy uv sync(默认硬链接) 7.2 s 1.8 s 0.27 s 0.04 s
线性刻度,同一把尺。pip 的 7.2 s 里,建空环境占 2.5 s(要往里装 pip 自己),装包占 4.7 s。全部从零开始(连 Python 都要下载 28 MiB)的 uv sync 是 6.6 s。

约束 2:一个环境里一个包只能有一个版本,传递依赖总会撞车。撞车时,要么悄悄挑一个,要么当场说清楚。

Maven:最近者优先悄悄挑一个

app lib-a lib-b lib-x common 1.0 ✓ common 2.0 ✕ 路径 2,比 3 近,选它 构建通过,线上 NoSuchMethodError

uv:所有约束同时满足满足不了就报错

lib-a: common<2 lib-x: common>=2 求解器:找一组版本 × No solution found … 错在 uv lock 那一刻,lock 不会生成,更不会上线

uv 的求解器基于 PubGrub 算法,找不到解时会把冲突链一条条列出来。Maven 也是一个类只能有一个版本,区别只在冲突时它按规则挑一个,不报错。

约束 3:环境和锁文件会悄悄走散。人会忘记 sync,会手动装包,会改了 pyproject 忘记 lock。uv 的回答是让“运行”这个入口顺手把三样对齐:uv run 每次都检查,代价是每次多十几毫秒。

比方失效的地方:.venv 像 Maven 的 target/(可删、不进 git),但 target 放的是编译产物,.venv 放的是运行环境,Java 没有对应物,因为 classpath 不需要落地成目录。uv.lock 也不完全等于 pom.xml:pom 只写死直接依赖,传递依赖靠规则推,没有哈希;uv.lock 记的是整棵树加哈希,更像 Gradle 的 dependency locking 或 npm 的 package-lock.json。

基本语法十个命令,对着 Maven 记

uvMaven / Java 里做什么
uv init gc-helper --no-packagemvn archetype:generate建项目:pyproject.toml、.python-version、.gitignore、main.py,并 git init
uv python pin 3.13sdk use java 17写 .python-version;没装就在第一次用时下载
uv add httpx往 pom 里加一个 <dependency>改 pyproject,重新 lock,同步 .venv,一步到位
uv add --dev pytest<scope>test</scope>放进 [dependency-groups] dev,生产可以不装
uv remove httpx删掉那个 <dependency>同样三步一起做
uv syncmvn dependency:resolve按 lock 造 .venv,多的删、缺的补
uv run uvicorn main:appmvn spring-boot:run检查、同步,再在 .venv 里运行命令
uv treemvn dependency:tree看依赖树
uv lock --upgrade-package httpx改 pom 里的版本号只升级这一个包,其余不动
uvx ruff check下载一个 jar 用 java -jar 跑一次在临时环境里跑一个工具,不进项目依赖

参数、私服、Docker、CI 写法这些细节放到 第⑥步。

实验台:uv run 这一下到底做了什么先猜,再运行

$ uv run uvicorn main:app

先猜:这条命令会怎么走?

也可以不猜直接运行。每一格的耗时:标“实测”的是本机跑出来的,其余是估算。

1 找 Python.python-version

等待运行

2 对锁pyproject ↔ uv.lock

等待运行

3 同步环境uv.lock → .venv

等待运行

4 起进程.venv/bin/uvicorn

等待运行

⑤跑起来:用 uv 把值班助手从零搭起来

就是全景里那个服务:/gc/summary 同时拉 5 台机器的 GC 日志,order-04 超时。下面每一段输出都是本机原样跑出来的。

0装 uv(一个 Rust 写的单文件程序,不依赖本机的 Python)
# macOS / Linux,官方安装脚本
curl -LsSf https://astral.sh/uv/install.sh | sh
# 或者 brew install uv;Windows 用 winget install astral-sh.uv
uv --version          # 本页用的是 uv 0.13.7
1建项目实测
$ uv init gc-helper --no-package --python 3.13
Initialized project `gc-helper` at `…/gc-helper`
$ cd gc-helper && ls -A
.git  .gitignore  .python-version  README.md  main.py  pyproject.toml

--no-package:值班助手是一个服务,不需要打成包给别人装。本机的 uv 0.13.7 不加这个参数时,会生成带 src/ 目录和 [build-system] 的包结构,见 ⑥ 的第 9 条。

2加依赖(uvicorn[standard] 要加引号,zsh 会把方括号当通配符)实测
$ uv add fastapi 'uvicorn[standard]' httpx
Using CPython 3.13.18
Creating virtual environment at: .venv
Resolved 24 packages in 9ms
Installed 23 packages in 13ms
 + fastapi==0.162.2  + httpx==0.28.1  + uvicorn==0.54.0  + uvloop==0.23.0 …
$ uv add --dev pytest
Resolved 30 packages in 10ms
Installed 5 packages in 7ms

这是缓存热的时候。这台机器第一次跑(要下载 Python 和全部包)是 6.6 s。

pyproject.toml(uv add 写进去的)
[project]
name = "gc-helper"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
    "fastapi>=0.162.2",
    "httpx>=0.28.1",
    "uvicorn[standard]>=0.54.0",
]

[dependency-groups]
dev = [
    "pytest>=9.2.0",
]
uv tree --depth 1实测
gc-helper v0.1.0
├── fastapi v0.162.2
├── httpx v0.28.1
├── uvicorn[standard] v0.54.0
└── pytest v9.2.0 (group: dev)
uv.lock 开头
version = 1
revision = 5
requires-python = ">=3.13"

[[package]]
name = "anyio"
version = "4.16.0"
source = { registry = "https://pypi.org/simple" }
wheels = [ { url = "…", hash = "sha256:c66a…" } ]
3放代码,起服务main.py、fakehosts.py 就是 全景页里那两个文件实测
# 终端 1:假装 5 台机器的日志代理,order-04 故意 5 秒才回
$ uv run uvicorn fakehosts:app --port 9100
# 终端 2:值班助手本身
$ uv run uvicorn main:app --port 8000
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
# 终端 3
$ curl -s 'localhost:8000/gc/summary?service=order&minutes=10'
elapsed_ms: 1151
{'host': 'order-01', 'pauses': 60, 'max_ms': 256.153, 'avg_ms': 33.3, 'error': None}
…
{'host': 'order-04', 'pauses': 0, 'max_ms': None, 'avg_ms': None, 'error': 'timeout 1s'}
{'host': 'order-05', 'pauses': 60, 'max_ms': 253.093, 'avg_ms': 30.7, 'error': None}

1151 ms 是服务刚起来后的第一次请求,多了建连接的开销;全景页里再跑一次是 1009 ms。用 ps 看,uv run 是父进程,真正干活的是它起的子进程 .venv/bin/python .venv/bin/uvicorn main:app。

4测试,提交实测
$ uv run pytest -q
2 passed, 1 warning in 0.31s
$ git add pyproject.toml uv.lock .python-version *.py && git commit -m "gc-helper 骨架"
# .venv 已经在 uv init 生成的 .gitignore 里,不会被提交

那 1 个 warning 是 Starlette 提示它的 TestClient 以后要换 httpx2,和 uv 无关,pytest 那一页再讲。

到这里已经能交差了:同事 git clone 以后只要一句 uv run uvicorn main:app,Python、依赖、环境全都自动备好,版本和你的一模一样。
已知还没处理的细节

⑥细节·规范·优化

按圈往深处走:先是不知道就会出事的坑,再是团队里该怎么写,最后是为什么快、锁文件怎么覆盖所有平台。

第一圈

注意事项:九个坑

1钻进 .venv 手动装包,下次 sync 就没了← ④ 环境是算出来的
现象
uv pip install six 以后,uv run 还能用;一跑 uv sync,输出 - six==1.17.0,被删了。实测
根源
sync 默认精确同步,lock 里没有的就删。uv run 不删多余的包,所以问题会藏一阵子。
做法
一律 uv add six。
2uv.lock 没提交← ④ 两个文件分工
现象
同事 clone 下来,uv 按 pyproject 的范围重新解析,拿到更新的版本,“我这能跑,你那报错”。
根源
pyproject 只写范围,确定版本的是 lock。
做法
应用项目一定提交 uv.lock。
3CI 里只写 uv sync← ④ uv run 自带检查
现象
有人手改了 pyproject 没跑 lock,CI 悄悄重新解析,测的是另一套版本,还照样绿灯。
根源
sync 和 run 发现走散会自动修好 lock,在本地是方便,在 CI 是掩盖。
做法
uv sync --locked:走散就报 The lockfile at `uv.lock` needs to be updated, but `--locked` was provided. 实测
4直接 python main.py,用的是系统 Python← ④ venv 是一个目录
现象
本机 python3 是 3.10,报 ModuleNotFoundError: No module named 'fastapi'。实测 IDE 没选解释器时也一样。
根源
包装在 .venv 里,只有 .venv/bin/python 会去那里找。
做法
用 uv run;IDE 里把解释器指到 .venv/bin/python。
6把 .venv 拷走或挪项目目录← ④ venv 是一个目录
现象
挪完以后 .venv/bin/uvicorn 报找不到解释器。
根源
脚本第一行写死了绝对路径:#!/…/gc-helper/.venv/bin/python。实测 bin/python 也只是指向 uv 装的解释器的链接。
做法
不拷 .venv,到新位置 uv sync 重建,几十毫秒。
7私服加公网 PyPI,被同名包抢注← ④ 约束 2 冲突要说清
现象
pip 的 --extra-index-url 会在所有源里挑版本最高的:有人在公网发一个和你内部包同名、版本号更大的包,就被装进来了(依赖混淆攻击)。
根源
“哪个源都行,挑最新”是 pip 的老规则。
做法
uv 默认 first-index:一个包在哪个源先找到,就只用那个源。私服写进 [[tool.uv.index]],国内镜像用 default = true 替换 PyPI。--index-url 已改名为 --default-index。
8uv add 只写下限← ④ 两个文件分工
现象
写进去的是 fastapi>=0.162.2,没有上限。实测
根源
应用项目有 lock 兜底,下限够用。要发布给别人用的库不一样:别人装你的库时不读你的 lock,上游出一个不兼容的大版本就会坏。
做法
应用:保持默认,靠 lock。库:按需加上限,比如 "httpx>=0.28,<1"。
9uv init 默认建成了“包”版本行为
现象
本机 0.13.7 的 uv init gc-helper 生成 src/gc_helper/ 和 [build-system],每次 sync 都会先构建、安装项目本身。实测
根源
不是原理推出来的,是 uv 选的默认值,各版本不一样。
做法
服务和脚本加 --no-package;要发布的库才用默认或 --lib。
第二圈

使用规范:团队里这么写

提交三样:pyproject.toml、uv.lock、.python-version

.venv 不提交,uv init 已经写进 .gitignore。

同事装出来和你不一样。

改依赖只用 uv add / uv remove

不手改依赖列表,更不手改 uv.lock。

pyproject 和 lock 走散。

CI 和镜像构建用 uv sync --locked

生产镜像再加 --no-dev,pytest 这些不进生产。

CI 测的不是提交的那一套。

升级单独提交:uv lock --upgrade-package httpx

lock 的 diff 和改 pom 版本号一样走代码评审。

一次升级混进一堆无关的版本变化。

统一 uv 版本

在 pyproject 里写 [tool.uv] required-version = ">=0.13,<0.14"。

uv 还在 0.x,不同版本的默认行为会变。

工具分两类放

项目要用的开发工具(ruff、mypy)放 dev 组;偶尔跑一次的用 uvx。

运行依赖越来越胖,生产镜像跟着变大。

第三圈

深处:为什么快、锁怎么覆盖所有平台、进程长什么样

为什么快

  • 缓存加链接:本机实测,同样的包,缓存热时硬链接 0.04 s,强制复制 0.27 s,pip 7.2 s。
  • 只读元数据:解析时需要知道每个版本依赖谁。PyPI 能单独提供 wheel 里的元数据文件(PEP 658),uv 不用把整个包下载下来。(按文档)
  • Rust、并行:下载和解压并行做;uv 本身是一个原生程序,不用先起 Python。

一份锁怎么覆盖所有平台

  • 解析碰到“只在某些平台才要”的依赖(比如带 sys_platform != 'win32' 标记的 uvloop),就按标记把求解拆成几支分别解,最后合成一份 lock。(按文档)
  • 所以 lock 里有不属于本机的 wheel:本机实测有 20 个 Windows、56 个 macOS 的条目。
  • sync 时用本机的平台和 Python 版本去套标记,只装套得上的。

uv run 的进程模型

  • 实测 ps:uv 留作父进程,子进程是 .venv/bin/python .venv/bin/uvicorn …。
  • 按文档,uv 会把 Ctrl-C、SIGTERM 等信号转给子进程,所以在终端和容器里都能正常停。
  • 生产镜像里常见的写法是先 uv sync --locked --no-dev,启动命令直接写 .venv/bin/uvicorn:少一层进程,也省掉每次启动的检查。

字节码要不要预编译

  • Python 第一次 import 一个模块时把它编译成 .pyc 存起来。uv 安装时默认不做这一步,所以装得快。
  • 镜像里每个容器都是新的文件系统,每次启动都要重新编译。设 UV_COMPILE_BYTECODE=1(或 --compile-bytecode),构建慢一点,启动快一点。(按文档)
版本差异(只给结论)
  • uv 自己:还在 0.x,一年发很多版。本机 0.13.7 的 uv init 默认生成 src 布局的包;网上较早的教程里生成的是扁平的 main.py。照着教程敲时,先对一下 uv --version。
  • 源的参数:--index-url 和 --extra-index-url 在 0.13.7 的帮助里已标为弃用,换成 --default-index 和 --index。本机核对
  • Python:值班助手要 3.11 以上(asyncio.timeout),本页用 3.13。requires-python 写低了,lock 会替所有更老的版本一起求解,可能挑出更旧的包。

下一圈

  • workspace:一个仓库多个包,约等于 Maven 多模块
  • 单文件脚本带依赖(PEP 723):uv add --script,值班助手的小工具不用建项目
  • 用 uv 写 Dockerfile:多阶段构建、缓存挂载、非 root 运行
  • 老项目迁移:从 requirements.txt 或 Poetry 搬到 uv
  • 打包和发布:uv build、uv publish,发到公司私服
  • 要 GPU 时:Conda、pixi 和 uv 怎么配合

↺回到⑤:拿⑥改进值班助手

三个练习,每个都在⑤的项目上用一条⑥的内容。

让 CI 守住锁

手改 pyproject 加一行 "orjson",分别跑 uv run --locked python -c 1 和 uv run --frozen python -c 'import orjson',看哪个报错、报什么,再用 uv add orjson 改对。

用到:第 3 条,实验台第 4、5 个场景

给值班助手写 Dockerfile

先复制 pyproject.toml 和 uv.lock,跑 uv sync --locked --no-dev,再复制代码,启动命令写 .venv/bin/uvicorn main:app --host 0.0.0.0,镜像里设 UV_COMPILE_BYTECODE=1。

用到:第 5 条,第三圈的进程模型和字节码

走镜像源

在 pyproject 里加一个 [[tool.uv.index]],写一个国内镜像并设 default = true,删掉缓存再 uv lock,看 uv.lock 里的 source 变成了什么。

用到:第 7 条

自测

1. 用一句话说出 uv 的精华。

pyproject 写意图、uv.lock 钉死整棵依赖树、.venv 按锁算出来;包从全局缓存硬链接,所以环境可以随手删、随时重建,并且永远和锁一致。

2. pip + venv 的老办法,痛在哪?

Python 版本不归它管;pip freeze 只记录这台机器此刻的状态,没有哈希、不分直接和间接依赖、不管别的平台;每个环境都要完整解压复制一遍,慢。

从 ② 和 ④ 约束 1 来。

3. 手动装进 .venv 的包为什么会被删?是哪条原理?

路是单向的:.venv 由 uv.lock 算出来,sync 默认精确同步,lock 里没有的包就删。

从 ④ 核心机制第 3 点来。

4. 预测:rm -rf ~/.cache/uv 以后,已经建好的 .venv 还能用吗?下次重建呢?

能用。硬链接是同一份数据的两个名字,删掉缓存这个名字,.venv 里那个名字还在,数据还在(链接数从 2 变成 1)。下次重建 .venv 时缓存是空的,要重新下载,从几十毫秒变成秒级。

从 ④ 约束 1 的 inode 图推出来,页面上没有原话。

5. 预测:同事在 Windows 上 uv sync,会装 uvloop 吗?uvicorn 还能跑吗?

不装。lock 里 uvloop 带着 sys_platform != 'win32' 的标记,sync 用本机平台去套,套不上就跳过。uvicorn 能跑:它发现没装 uvloop,就用 Python 自带的 asyncio 事件循环,只是慢一点。

从 ④ 核心机制第 2 点和 ① 的图推出来。

6. 放回全景:第 1 站前后各发生了什么?uv 这一步坏了,整条链路会怎样?

前面:你(或 systemd、容器)敲下 uv run uvicorn main:app。后面:第 2 站 uvicorn 在 .venv 里起来,监听 8000 端口,按装没装 uvloop、httptools 选事件循环和 HTTP 解析器,然后等 curl。

第 1 站整个失败(Python 下载不了、私服连不上、--locked 报错),就没有进程在听,curl 直接收到 Connection refused,后面 9 站一站都不会发生。更隐蔽的是装“错”了:比如 uvicorn[standard] 写成了 uvicorn,uvloop 和 httptools 没装,服务照样起来,但第 2 站和第 7 站换成了更慢的实现;或者 lock 没提交,值班机上的 httpx 是另一个版本,第 6 站的超时行为可能就变了。

从 ④ 和 全景第 2 站推出来。