☰
Python依赖管理新利器uv:常用命令与工程实践解析
2026/10/14 4:39:29 网站建设 项目流程

如果你和我一样,过去几年一直被 Python 项目的环境问题折腾,比如虚拟环境忘激活、依赖装错环境、换台机器就复现不了,那你看到 uv 这套命令时,多半会有一种“这次终于有人把常用操作收敛清楚了”的感觉。简单说,uv 是一套面向 Python 开发场景的命令行工具,它把依赖安装、虚拟环境、版本锁定、Python 解释器管理、甚至包构建发布都收进了同一条工作流里。我接下来要聊的,就是日常最常用的 uv 命令,每个命令解决什么问题、为什么这样设计、以及我迁移后踩过的坑。适合刚接触 uv 的新手,也适合正打算从传统 pip 流程切换过来的老手。

1. 换到 uv 之前,我先想清楚了一个问题:它省掉的到底是什么

1.1 传统工作流里,时间都耗在了“环境治理”上

以前做一个 Python 项目,标准流程大概是:先python -m venv .venv创建虚拟环境,再source .venv/bin/activate激活,接着pip install -r requirements.txt安装依赖。每次打开新终端,第一步永远是激活环境。如果同时维护三四个项目,很容易混淆当前到底在哪个环境里,有时候明明刚装过包,换个终端又找不到了,最后干脆把依赖装到系统 Python 里,一时省事,后面全是麻烦。

依赖一多,传统方式的问题会更明显。requirements.txt只记录直接依赖和某个时刻解析出来的版本,但传递依赖到底是谁引入的、版本是哪个,完全看不出来。某天一个间接依赖升级,可能引发一串连锁报错。这时候你只能靠pip freeze把整个环境导出来,但 freeze 出来的文件非常“实况”,本地环境是什么样,锁定的就是什么样,换个平台经常还是出问题。

1.2 uv 的设计思路:把项目目录当作唯一上下文

uv 的核心设计之一,是让命令基于项目目录自动工作,而不是依赖 shell 里的激活状态。它默认会在项目里创建.venv,当你执行uv run或uv sync时,它会自动定位到这个环境,不再要求你先执行 activate。所有依赖信息都集中在pyproject.toml和uv.lock里,项目目录就是唯一上下文。

这种收敛带来的好处很直接:你不用再问“我现在的环境是哪个”,你在哪个项目目录下执行命令,uv 就操作哪个项目的环境。再加上 uv 的底层是用 Rust 实现的,依赖解析和下载速度确实快,我用uv sync重建一个中等规模项目的环境,体感从以前的分钟级降到秒级。

1.3 高频 uv 命令总览

先给一张速查表,后面每个命令都会展开细讲:

命令主要作用典型用法
uv init初始化新项目结构uv init demo-project
uv add添加依赖并同步环境uv add <包名>
uv remove移除依赖uv remove <包名>
uv sync按锁文件同步虚拟环境uv sync
uv lock生成或更新锁定文件uv lock
uv run在项目环境中执行命令uv run python main.py
uv python管理 Python 解释器版本uv python install 3.12
uv tool隔离安装全局命令行工具uv tool install <工具名>
uvx临时运行一个工具uvx <工具名> <参数>
uv cache查看和清理全局缓存uv cache clean
uv build构建 wheel 和 sdistuv build
uv publish发布包到包索引uv publish

1.4 什么项目适合全面切到 uv

我的判断是:绝大多数 Python 项目都适合。个人脚本、库开发、Web 后端、数据分析项目,都能从这套命令里获益。唯一的例外可能是那种只有几十行、没有任何第三方依赖的临时脚本,这种直接用系统 Python 跑就行,没必要引入额外工具。

一旦项目出现“换台机器就装不上”“本地能跑 CI 挂”“不同人环境版本不一致”这类问题,就该考虑整体切换到 uv。它不是要多学一堆新概念,反而是把原来散落在多个工具里的概念统一到一起。

2. 新建项目三件套:uv init、uv add、uv remove 怎么配合

2.1 uv init 一次生成项目骨架

创建新项目最直接的方式就是uv init demo-project。执行后,目录里会生成pyproject.toml、.python-version,以及一个包目录或模块文件。pyproject.toml是整个项目的配置中心,几乎所有依赖声明、构建配置、工具配置都写在这个文件里。

.python-version则记录当前项目期望使用的 Python 版本,由uv python pin维护。这一下就把“项目元信息”和“环境元信息”都固定住了。我第一次用的时候有点意外,没想到 init 能直接生成这么完整的骨架,因为以前创建项目往往要手动补一堆配置文件。

2.2 uv add:添加依赖的正确姿势

给项目添加依赖,最标准的操作是:

uv add <包名>

这条命令会做三件事:解析依赖的最新兼容版本、把依赖写入pyproject.toml的dependencies字段、更新uv.lock锁定文件,然后立即同步到当前项目的.venv里。也就是说,添加依赖后不需要再手动执行 install。

如果需要区分开发环境和生产环境的依赖,可以使用:

uv add --dev <包名>

这样依赖会被写进开发依赖组,发布包时不会带上。我在实际项目里,通常会把测试、代码检查相关的包放在--dev,运行时核心依赖放在普通 dependencies 中。

2.3 uv remove:删除依赖也要同步三处

移除依赖对应的是uv remove <包名>。它同样会更新pyproject.toml、uv.lock和虚拟环境三处。这看起来简单,但恰恰是很多人容易忽略的点:如果直接手工删除pyproject.toml里的某个依赖,却没有更新锁文件和虚拟环境,那项目的声明状态和实际运行状态会不一致,后续排查问题会很痛苦。

2.4 手工编辑 pyproject.toml 后的正确操作

虽然 uv 命令已经覆盖了大多数场景,但偶尔你还是会想直接编辑pyproject.toml,比如调整版本范围、修改项目元数据。编辑完之后,正确的流程是:

uv lock uv sync

第一步更新锁定文件,第二步让虚拟环境与锁文件保持一致。如果你确信只是改了个描述信息,不需要重新解析依赖,也可以直接uv sync,它会自动检测pyproject.toml是否变化。但为了 CI 里能稳定复现,最好养成“先 lock 后 sync”的习惯。

2.5 我踩过的小坑:手工加依赖导致版本冲突

有次我在pyproject.toml里手工加了一个包,没写版本范围,uv lock 时自然选了最新版。过了两天另一个依赖升级,两者之间出现不兼容,整个项目直接跑不起来。从那以后我尽量使用uv add而不是手工编辑,因为uv add会把当前解析结果和锁定文件一起更新,出问题的概率小很多。

3. uv run:为什么它能让我彻底忘掉 activate

3.1 activate 的痛点:环境状态是“记忆”出来的

过去每次打开终端,第一件事就是激活虚拟环境。问题在于,环境状态是存在 shell 会话里的,不是存在项目目录里的。你一旦开了新终端,就得重新激活;忘了激活,python指向的是系统解释器,依赖全部找不到。

还有个隐性成本:activate 修改的是环境变量,在多终端、多项目并行时非常容易出错。很多人到最后干脆全部装到全局环境,用“不管那么多了”的心态工作,后续代价更高。

3.2 uv run 的自动环境定位

uv run的设计思路完全不同。你只需要执行:

uv run python main.py

它会先检查当前目录的项目状态:如果.venv不存在,就自动创建;如果依赖与锁定文件不一致,就自动同步;然后在这个环境中执行你给出的命令。所有这些判断,都基于项目目录,而不是 shell 状态。

除了跑 Python 脚本,uv run也可以执行任意命令:

uv run pytest uv run python -m http.server

你甚至可以用它运行项目里安装的命令行工具,比如代码格式化工具或静态检查工具,不需要手动进入环境。

3.3 临时依赖注入

有时候我只想临时用某个工具跑一次,又不想把它写进项目依赖。这种场景用--with参数最合适:

uv run --with <临时包名> python -c "import demo; print(demo.__version__)"

命令执行时,uv 会临时准备一个包含这些包的环境,跑完即弃,不污染项目配置。这个能力在做快速验证、临时对比不同库版本时非常有用。

3.4 几个重要参数

  • --no-sync:跳过自动同步,直接用当前已有的虚拟环境执行命令。
  • --frozen:要求锁定文件存在且与pyproject.toml一致,不执行任何更新,这在 CI 中非常关键。
  • --isolated:不使用项目虚拟环境,类似临时开一个干净环境。

这些参数解决的是同一个问题的不同侧面:什么时候允许改动环境,什么时候必须保持现状。

3.5 实际感受

我真正依赖上uv run,是在一次给同学演示项目启动步骤的时候。以前 README 里要写“先创建 venv,再激活,再安装依赖,最后运行”,现在只需要一句uv run python app.py。新手拿到项目,连虚拟环境是什么都不用理解,就能把项目跑起来。这种体验上的简化,比单纯的速度提升更重要。

4. uv lock 与 uv sync:从“能跑”到“可复现”

4.1 两种文件的分工

pyproject.toml里的依赖通常写的是版本范围,比如>=1.0,<2.0,意思是允许 1.x 系列内的升级。这个范围保证了灵活度,但也意味着今天装出来的环境和三个月后装出来的环境可能不同,因为没有锁定具体版本。

uv.lock则不同,它记录了解析后的精确版本,包括直接依赖和所有传递依赖,还有来源信息、校验信息。它回答的问题是:当前这一整套依赖,具体是哪一批版本。只要锁定文件存在,任何人都能重建出几乎一致的环境。

4.2 uv lock 什么时候需要手动执行

大多数情况下,uv add会自动更新锁文件,不需要手动执行。但遇到以下几种情况,我会手动跑:

  • 手工修改了pyproject.toml里的版本范围;
  • 想要升级某个特定包到最新兼容版本;
  • 想要把全部依赖升级一遍。

升级单个包可以这样:

uv lock --upgrade-package <包名>

全部升级则是:

uv lock --upgrade

4.3 uv sync 的安装逻辑

uv sync会读取uv.lock,把里面记录的精确依赖安装到项目虚拟环境。它可以理解为“让当前环境与锁文件对齐”。当克隆一个新项目、切到新分支、或者同事更新了锁文件之后,我都直接执行一句话:

uv sync

如果是在 CI 流水线里,我更推荐加上--frozen:

uv sync --frozen

这样如果uv.lock与pyproject.toml不一致,命令会直接报错,而不是悄悄重新解析。CI 必须保持确定性,不能让一次自动更新悄悄改变测试环境。

4.4 一个容易出问题的点:不要手动改锁文件

uv.lock不是给人手工编辑的文件。它格式严格,内容由解析算法生成。有人试图“手动清理”里面的某些包,结果导致锁文件与pyproject.toml不一致,后面每次执行命令都会出现莫名其妙的同步错误。

如果锁文件已经损坏或混乱,最简单的恢复方式是把锁文件删掉,重新执行:

uv lock uv sync

4.5 与旧方案 pip freeze 的核心差别

旧方案常用pip freeze > requirements.txt,本质是把某个环境里的“实况快照”导出来。问题在于,这个快照包含了当前环境的特殊状态,不一定代表项目的真实依赖关系。uv.lock 记录的是从pyproject.toml出发、经过完整解析得到的依赖图,带来源和校验信息,更接近其他语言生态里的标准 lockfile 思路。简单说,一个是“环境备份”,一个是“项目依赖图”,后者的可复现性更强。

5. uv python:解决“本机没有那个版本”的尴尬

5.1 安装指定版本的 Python

很多项目的报错,归根结底是 Python 版本不对。传统做法是去官网下载安装包,或者依赖系统自带的版本。uv 提供了更直接的方式:

uv python install 3.12

它会自动下载对应版本的 Python 解释器,安装到 uv 自己的管理目录里,不干扰系统 Python。查看当前本机已有和可用的版本可以执行:

uv python list

5.2 给项目固定开发版本

项目内希望统一 Python 版本时,使用:

uv python pin 3.11

这会在项目根目录生成.python-version文件。之后所有uv run、uv sync都会优先使用这个版本。团队成员拿到项目,不用手动安装解释器,只要执行命令,就能在需要时自动下载并使用对应版本。

要注意一个区分:.python-version决定的是开发环境用哪个解释器;pyproject.toml里的requires-python决定的则是这个包本身支持哪些 Python 版本。二者需要配合,但并不是一回事。库项目尤其要把requires-python写清楚,否则用户可能在不支持的版本上安装。

5.3 多版本并行验证

当你需要验证代码在不同 Python 版本下的表现,可以临时指定:

uv run --python 3.10 python -c "print('hello')"

这个能力在 CI 矩阵测试里也很方便:项目只需一个锁定文件,但可以在多个 Python 版本上分别运行测试。实际体验下来,比自己在系统里装一堆解释器再手动切换要省心得多。

5.4 uv python 子命令汇总

子命令作用
uv python install安装指定版本
uv python list查看可用版本
uv python find查找某个版本的解释器路径
uv python pin给项目固定版本
uv python uninstall卸载不再需要的版本

5.5 一个小提醒

自动下载 Python 很省事,但如果你在公司内网、网络受限,可能需要提前把解释器包准备好,或者配置本地镜像源。我遇到过一次自动下载失败的情况,处理方式是在本机先uv python install 指定版本,等它进入缓存后再操作项目,后面就顺畅了。

6. uv tool 与 uvx:把命令行工具装进“独立包间”

6.1 全局安装工具的老毛病

有些工具是跨项目使用的,比如代码格式化、静态检查、项目管理类命令行工具。以前我习惯用pip install直接装到全局,后来发现两个问题:一是工具和项目依赖会混在一起,升级工具可能导致某个项目环境出问题;二是卸载工具时往往残留一堆依赖包,全局环境越来越脏。

6.2 uv tool install 的隔离安装

uv 提供了一个专门管理这类工具的子命令:

uv tool install <工具名>

它会为这个工具单独创建一个环境,然后在用户目录里生成一个可执行命令入口。以后你在任意目录直接敲工具名,都能运行,但它不会污染任何项目环境。

常用的管理命令:

uv tool list # 查看已安装的工具 uv tool upgrade <工具名> # 升级某个工具 uv tool uninstall <工具名> # 卸载某个工具 uv tool dir # 查看工具环境的根目录

6.3 uvx:连安装都省了

如果你只是偶尔用一次某个工具,不想长期维护它,可以用uvx:

uvx <工具名> <参数>

uvx会在临时环境里解析并运行这个工具,用完即走。它有点像一个“免安装的运行器”,特别适合临时处理数据文件、格式转换这类低频任务。

6.4 工具环境与项目环境的边界

这里要强调一下:项目依赖永远用uv add和uv sync管理,全局命令行工具用uv tool管理。两者不要混用。我见过有人为了方便,把一个项目的依赖用uv tool install装了,结果命令入口全局可用了,但项目环境反而没装齐,构建时到处报 ModuleNotFoundError。

6.5 我的使用习惯

我现在所有跨项目工具都走uv tool,项目内依赖走uv add。这样每次重装系统或换电脑,只需要执行一条命令导入工具列表,就能快速恢复日常使用的命令行环境,项目内的依赖则各管各的,互不干扰。

7. uv cache 与离线场景:速度和安全感的来源

7.1 缓存机制:让重复安装变快

uv 会在全局维护一个缓存目录,下载过的包都会保存在里面。再次安装同一个包时,它可以直接从缓存复制或硬链接,而不是重新下载。这就是为什么同一个项目在多台机器、多个分支之间切换时,uv sync会感觉越来越快。

查看缓存目录位置:

uv cache dir

7.2 常见缓存管理命令

  • uv cache clean:清空全部缓存,释放磁盘空间。
  • uv cache prune:只清理不再需要的条目,保留可能还会用到的缓存。
  • uv cache dir:查看当前缓存路径。

我一般不会随便执行clean,因为缓存是速度保障。只有在磁盘明显吃紧时,才会先prune再决定是否clean。

7.3 离线安装场景

uv 支持离线模式:

uv sync --offline uv add --offline <包名>

只要所需依赖已经在缓存中,离线状态下也能完成安装或同步。这在内网部署、外出办公但网络不稳定的情况下非常实用。我第一次在无网络环境里成功重建项目环境时,最大的感受是:提前让缓存“吃饱”,比临时下载要可靠得多。

7.4 缓存位置与链接方式的配置

缓存目录可以通过环境变量修改,比如:

export UV_CACHE_DIR=/path/to/cache

另外 UV_LINK_MODE 可以控制硬链接还是复制方式,不同文件系统下的表现不同。建议如果缓存目录和项目在同一块磁盘上,优先使用硬链接,安装速度快且不额外占用空间。具体效果和平台有关,实测为主。

7.5 缓存占空间的误区

缓存目录往往有几个 GB,看着吓人,但这是“用空间换速度”的典型场景。只要不过度膨胀,我倾向于不清理。尤其是喜欢在不同分支间切换的人,缓存命中一次就能省下升级依赖的大量时间,这笔账很划算。

8. uv build 与 uv publish:项目收尾的最后一公里

8.1 构建分发包

当项目需要发布给其他人安装时,要先构建出可分发的产物。执行:

uv build

默认会在dist/目录下生成 wheel 和 sdist 两种格式。wheel 是常见的安装格式,sdist 是源码包。使用 uv build 前,需要在pyproject.toml里配置好[build-system],否则无法识别如何构建。

8.2 发布到包索引

构建完成后,发布命令非常短:

uv publish

发布需要凭证,通常通过环境变量提供令牌,避免把密钥写进命令历史或配置文件。例如:

UV_PUBLISH_TOKEN=<你的令牌> uv publish

如果发布到私有索引,还需要指定仓库地址,例如:

uv publish --publish-url <仓库地址>

8.3 发布前的重要检查

发布后同名同版本的文件通常不允许覆盖,所以发布前我会习惯做一套检查:

  • 确认pyproject.toml里的版本号已经更新;
  • 确认 README、许可证等元数据文件存在且内容正确;
  • 至少跑一遍测试命令;
  • 检查dist/里的构建产物文件列表是否齐全;
  • 实际在一个干净环境里安装一次构建产物,验证可用。

这些步骤每一条都很基础,但漏掉任意一条都可能导致发布出去的包无法使用,或者用户拿到的是缺失文件。

8.4 本地验证产物的方式

验证构建产物时,可以用uv run --with <本地wheel路径> python -c "import 包名"之类的临时环境方式,确保它不依赖当前项目的虚拟环境。这样可以模拟“用户拿到包之后从零安装”的真实场景,避免出现“本地能跑,别人装不上”的情况。

8.5 为什么要区分 build 和 publish

构建和发布分成两条命令,看起来多了一步,但实际上避免了“边构建边上传”的耦合。构建出的产物可以先检查、测试、签名,确认无误后再发布。如果构建产物有问题,你只需要重新构建,不需要担心已经上传的坏版本。

最后说一点个人体会。用了这么久,我最喜欢的其实不是某个单独命令,而是这套命令之间的连贯性:uv init出来一个项目,uv add添加依赖,uv lock和uv sync锁定环境,uv run执行代码,最后uv build和uv publish收尾。整个过程几乎没有“切工具”的断裂感。偶尔我还是会手工编辑pyproject.toml,但只要记住改完就跑一遍uv lock && uv sync,基本不会出乱子。如果你也想减少在环境管理上浪费的时间,从一个新项目开始尝试这套流程,是最没有风险的方式。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询