首页/Python 与数据实践/工程化工具链
Python 与数据实践 · 第 23 章

工程化工具链

上一章走完发布全流程的同学应该有体感:为了发布一个小小的库,我们动用了 venv 建环境、pip 装工具、build 做打包、twine 传产物……工具四散、命令一长串,换个工程还得从头再来一遍。多版本 Python 的…

约 18 分钟7,313 字33 个代码示例

上一章走完发布全流程的同学应该有体感:为了发布一个小小的库,我们动用了 venv 建环境、pip 装工具、build 做打包、twine 传产物……工具四散、命令一长串,换个工程还得从头再来一遍。多版本 Python 的管理之前也是另一套工具在管。

这一章换个思路:把整个工具箱换成一件趁手的兵器——uv。

官方文档:https://docs.astral.sh/uv/


1. 【可选】先送走 pyenv

前面章节用 pyenv 装多版本 Python。uv 自己就带完整的版本管理(第 4 节会讲),pyenv 可以功成身退了。不卸也不影响使用 uv,纯属清爽起见。

macOS(Homebrew 装的)

# 1. 卸载本体
brew uninstall pyenv

# 2. 清掉残留目录
rm -rf ~/.pyenv

# 3. 打开 ~/.zshrc,删掉当初加的这两行:
#    eval "$(pyenv init -)"
#    export PYTHON_BUILD_MIRROR_URL="..."
#    改完执行 source ~/.zshrc 生效

Windows(pyenv-win)

# 1. 删掉安装目录(默认在用户目录下)
rm -r $env:USERPROFILE\.pyenv

# 2. 打开「系统环境变量」面板,逐项清理:
#    - 用户变量 PYENV
#    - 用户变量 PYTHON_BUILD_MIRROR_URL
#    - Path 里的 %USERPROFILE%\.pyenv\pyenv-win\bin
#    - Path 里的 %USERPROFILE%\.pyenv\pyenv-win\shims

确认送走了

pyenv --version
# 提示 command not found: pyenv,说明清理干净了

2. uv 是何方神圣

uv 是 Astral 公司用 Rust 写的 Python 包管理与工程管理工具,野心是把上面那一堆零散工具全部收编:

被收编的工具 uv 的对应能力 干的事
pip uv pip 装包、卸包
pip-tools uv lock / uv sync 锁版本、对齐环境
pipx uv tool 运行/常驻 CLI 工具
virtualenv / venv uv venv 虚拟环境
pyenv uv python 多版本 Python
poetry / pdm uv init / uv add / uv remove 建工程、管依赖
build + twine uv build / uv publish 构建、发布(上一章的两步棋)

它的招牌是速度:官方口径比 pip 快 10~100 倍——并行下载、全局缓存、Rust 实现,装一批依赖经常是"回车还没松手就完事了"的体验。

3. 安装

# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows(PowerShell)
powershell -ExecutionPolicy ByPass -c "powershell -c \"irm https://astral.sh/uv/install.ps1 | iex\""

# macOS 也可以走 brew
brew install uv

装完验证一下:

uv --version

安装脚本会自动把 uv 加进 PATH。万一终端喊找不到命令,手动把 ~/.local/bin 补进 PATH 即可。

给下载提速:镜像源

国内网络直连 PyPI 经常龟速,建议配好镜像再干活。编辑 ~/.config/uv/uv.toml:

[[index]]
name = "company-private"
url = "公司私有源地址"
default = true

[[index]]
name = "tuna"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"

私有源放在 default 位置优先命中,公有镜像兜底——与上一章 pip 配置里 index-url / extra-index-url 的思路完全同构。

顺手改一下 Code Runner

如果你在用 VSCode 的 Code Runner 插件跑代码,把执行器换成 uv,运行时自动带项目环境:

"python": "uv run"

4. Python 版本管理:pyenv 的班 uv 来接

先看 uv 顺手替掉 pyenv 的那部分。

# 列出所有可安装的 Python 版本
uv python list

# 只看本地已经装了的
uv python list --only-installed

# 装指定版本
uv python install 3.12.3

# 不带参数 = 装最新稳定版
uv python install

# 一口气装多个
uv python install 3.11.9 3.12.3

# 卸载某个版本
uv python uninstall 3.12.3

# 当前环境实际用的是哪个解释器(打印路径)
uv python find

# 给当前工程钉死版本(目录下生成 .python-version 文件)
uv python pin 3.12

# 解除钉死(删掉 .python-version)
uv python pin --rm

# 查看当前工程钉的是哪个版本
uv python pin

# 临时换版本跑条命令,不动工程配置
uv run --python 3.11 python --version

# 建虚拟环境时直接指定版本
uv venv --python 3.12

# 也可以给一个版本范围,uv 会挑范围内可用的
uv venv --python ">=3.10"

# 带路径查看所有已装版本
uv python list --only-installed --verbose

重点记住 uv python pin:它把版本钉在 .python-version 文件里,跟着工程走。别人克隆你的工程后一条 uv sync,连 Python 版本都会自动对齐——协作时少吵很多架。

5. 五分钟起一个新工程

初始化

# 新建目录并初始化
uv init my-project
cd my-project

# 也可以对当前目录就地初始化
uv init

# 不想让它顺手建 git 仓库
uv init --vcs none

# 想要库工程的 src 布局(上一章 daylab-toolkit 那种)
uv init --lib

配一个命令行入口

uv init 默认不注册任何命令。想让工程支持 uv run xxx 直接执行、或发布后给用户一个终端命令,需要在 pyproject.toml 里加 [project.scripts]:

[project.scripts]
# 左边是命令名,右边是 "模块路径:函数名"
my-cli = "my_project:main"

配置生效后:

  • 工程内 uv run my-cli,就会调用 my_project/__init__.py 里的 main();
  • 一旦发布到 PyPI,用户 pip install 之后终端里就有 my-cli 这个命令可用。

加依赖

# 加一个运行依赖
uv add requests

# 加开发依赖(测试、格式化这类不随包发布的工具)
uv add --dev pytest

# 带版本约束地加
uv add "fastapi>=0.100.0"

uv add 敲下去,背后自动完成四件事:

  1. 解析整棵依赖树,挑出满足全部约束的最新版本;
  2. 把包安装进当前工程的虚拟环境;
  3. 把声明写进 pyproject.toml 的 dependencies;
  4. 生成/更新 uv.lock 锁文件,把每个包的精确版本和哈希钉死。

看看依赖长什么样:

# 打印依赖树(pipdeptree 的活儿)
uv tree

以本章作业工程 headline-lab 为例,输出形如:

headline-lab v0.1.0
├── aiofiles v25.1.0
├── httpx v0.28.1
│   ├── anyio v4.13.0
│   │   └── idna v3.17
│   ├── certifi v2026.5.20
│   ├── httpcore v1.0.9
│   │   ├── h11 v0.16.0
│   ├── idna v3.17
│   └── ...
└── python-dotenv v1.2.2

删依赖

uv remove requests

对齐环境

同事拉取了你的代码,或你想按锁文件把环境重建出来:

uv sync

它会按 uv.lock(没有锁文件就按 pyproject.toml)把依赖一步到位装齐,保证所有人的环境完全一致。

跑起来

# 跑某个文件
uv run src/main.py

# 以模块方式跑
uv run -m src.main

uv run 的妙处:它自动找到工程环境并在其中执行,不需要先 activate。从"进目录先激活环境"的老习惯里解放出来吧。

6. 虚拟环境:项目内 vs 项目外

uv 管环境有"跟项目绑定"和"独立使用"两种姿势。

# 当前目录建 .venv
uv venv

# 建到指定目录
uv venv my-env

# 指定 Python 版本
uv venv --python 3.13

激活与退出(老一套):

source .venv/bin/activate   # 进入
deactivate                  # 出来

查看信息:

uv venv --list   # 列出托管的环境(配合工程使用)

uv 在工程根目录创建 .venv 并自动识别。日常根本不用记 .venv 在哪、要不要激活——uv run 全替你料理了。

两种模式对照

模式 命令 适用
项目模式 uv add / uv sync / uv run 依赖写进 pyproject.toml,随工程走
全局模式 uv pip install / uv venv 不绑定工程,像传统 pip 一样用

全局模式兼容 pip 习惯,手头有老的 requirements.txt 也能直接喂给它:

uv pip install -r requirements.txt

日常优先项目模式:声明式管理,环境可复现,团队协作不扯皮。

全局缓存

uv 把下载过的包存在全局缓存(~/.cache/uv)里,多个工程共享同一份,装得飞快的秘诀之一。

# 看缓存目录在哪
uv cache dir

# 缓吃紧时清空
uv cache clean

7. 临时工具与常驻工具

开发中经常要临时用一把 ruff、black 之类的工具。老做法是装完用完再卸,麻烦且容易忘。uv 给了两个新姿势。

uvx:用完即走

# 不安装,直接跑
uvx ruff check .

# 等价于传统操作:
# pip install ruff && ruff check . && pip uninstall ruff

# 指定版本跑
uvx bandit@1.7.5 .

# 给工具本体传参时,用 -- 隔开
uvx cowsay -- "Hello, UV!"

uvx 的三步舞:临时建个环境 → 装包运行 → 用完即焚。

uv tool:常驻安装

打算长期用的 CLI 工具,装进工具区,终端全局可用:

# 安装
uv tool install ruff

# 之后直接敲命令
ruff check .

# 看装了哪些
uv tool list

# 升级
uv tool upgrade ruff

# 卸载
uv tool uninstall ruff

第三条路:作为开发依赖进工程

# 把工具声明为工程的开发依赖
uv add --dev ruff black mypy

随后在工程内:

uv run ruff check .
uv run black .
uv run mypy src/

这条路是团队协作的首选:工具跟着 pyproject.toml 走,同事一条 uv sync,工具和依赖一起就位,版本还完全一致。

8. 构建与发布:一条命令的事

上一章用 build + twine 两件套完成的事,uv 全包了。

构建

# sdist 和 wheel 一起出
uv build

# 产物落在 dist/ 目录
ls dist/
# headline_lab-0.1.0.tar.gz
# headline_lab-0.1.0-py3-none-any.whl

uv build 照样读 pyproject.toml 的 [build-system],后端还是 hatchling 那些老朋友——底层协议没变,只是前台换人了。

发布

# 发到 PyPI
uv publish

# 令牌建议走环境变量,别硬编码
UV_PUBLISH_TOKEN=pypi-xxxxx uv publish

# 发到私有仓库
uv publish \
  --publish-url 私有仓库地址 \
  --username 你的用户名 \
  --password 你的密码 \
  dist/*

uv publish 完整替代 twine upload。首次使用同样要先在 pypi.org 注册并创建 API token;uv 也认 .pypirc 配置文件。

9. Makefile:把常用命令钉在墙上

make 在 macOS / Linux 上开箱即有,Windows 默认没有,可任选其一补装:

# 方式一:Chocolatey(推荐)
choco install make

# 方式二:winget
winget install GnuWin32.Make

# 方式三:装 Git 时勾选 Git Bash,在 Git Bash 里用 make

装完验证:make --version

uv 已经把命令压缩了很多,但 uv run pytest、uv run ruff check .、uv build 这类组合,团队成员(以及三个月后的你自己)未必记得住。Makefile 的作用就是把这些操作"钉"成几个固定短词。

一份够用的工程 Makefile

.PHONY: install test lint format build clean

# 装齐依赖
install:
    uv sync

# 跑测试
test:
    uv run pytest

# 静态检查
lint:
    uv run ruff check .

# 自动格式化
format:
    uv run ruff format .

# 打分发包
build:
    uv build

# 清产物与缓存
clean:
    rm -rf dist/
    rm -rf .pytest_cache/
    uv cache clean

用起来:

make install   # 等于 uv sync
make test      # 等于 uv run pytest
make lint      # 等于 uv run ruff check .
make format    # 等于 uv run ruff format .
make build     # 等于 uv build
make clean     # 大扫除

接收参数的目标

.PHONY: upload

# 推送到指定仓库,用法:make upload REPO_URL=https://...
upload:
    uv publish --publish-url $(REPO_URL) dist/*
make upload REPO_URL=https://packages.example.com/simple

目标串目标

.PHONY: ci

# 本地过一遍 CI 流程:检查 → 测试 → 打包
ci: lint test build
make ci   # 依序执行 lint、test、build

(顺带纠个新手坑:上面 ci: lint test build 这一行依赖串接才是正确写法;把命令名直接写成一行 tab 缩进的 shell 命令是不行的。)

Makefile 的精髓是约定优于记忆:无论工程底层用什么工具链,新人只需要知道 make test 能跑测试、make build 能出包。CI/CD 脚本里引用同样的目标,本地和流水线行为天然一致。

10. 常用命令速查

命令 干什么
uv init 初始化工程
uv add 加依赖
uv remove 删依赖
uv sync 按锁文件对齐环境
uv lock 刷新锁文件
uv run 在工程环境里执行命令
uv tree 打印依赖树
uv build 打出 sdist + wheel
uv publish 发布到仓库
uv venv 建虚拟环境
uv python install 装 Python 版本
uv python list 列 Python 版本
uv python pin 钉死工程 Python 版本
uv tool install 常驻安装 CLI 工具
uv tool run / uvx 临时运行 CLI 工具
uv cache clean 清全局缓存

顺带预告:uv 还能跨工程管依赖(workspace 机制),把一个仓库里的多个包统一调度——这是下一章的主角之一,连同调试手段一起讲。

作业

  1. 把第 21 章的异步工程(或你手头任意一个 asyncio 程序)迁移成 uv 管理的工程:uv init 起骨架、uv add 声明依赖、uv python pin 钉版本,最后用 uv run 跑通。
  2. 给第 22 章的 daylab-toolkit 升个版本号,用 uv build 重新打包,并通过 uv publish 把它发到私有制品仓库;顺手为工程补一个 Makefile,把"构建 + 上传"封装成 make 目标。

下一章:24 · monorepo 与调试