进阶 2 · 依赖与包管理¶
环境搞定了(第 1 章),现在讲"依赖怎么声明、约束、锁定、复现"。这是工程化的核心——对照 Maven 的 <dependencies> + 锁定版本,Python 有自己的体系(PEP 440 版本约束 + uv.lock/requirements.txt)。本章 uv/pip 双路深入。
2.1 依赖声明:pyproject.toml¶
现代 Python 项目把依赖写在 pyproject.toml(第 10 章速览过,本章深入):
[project]
dependencies = [
"fastapi>=0.115",
"pydantic>=2.7,<3",
"sqlalchemy[asyncio]>=2.0.30",
]
[project.optional-dependencies]
dev = ["pytest>=8", "ruff>=0.5"]
-
Maven
-
Python (pyproject)
Python 的依赖声明比 Maven 简洁——一个字符串 "包名 版本约束",没有 groupId/坐标体系。
2.2 版本约束语法(PEP 440)¶
这是本章最该记牢的部分。版本约束决定"允许装哪些版本":
| 写法 | 含义 | pip 认? | uv 认? |
|---|---|---|---|
==1.2.3 |
精确钉死 | ✅ | ✅ |
>=1.2.3 |
至少 1.2.3,无上限 | ✅ | ✅ |
>=1.2,<2.0 |
范围(常用) | ✅ | ✅ |
~=1.2.3 |
兼容:>=1.2.3, ==1.2.*(允许 patch 更新) |
✅ | ✅ |
^1.2.3 |
caret:>=1.2.3, <2.0.0(允许 minor 更新,主版本锁定) |
❌ | ✅ |
!=1.2.0 |
排除某版本 | ✅ | ✅ |
* |
任意版本 | ✅ | ✅ |
⚠️ 关键差异:^ 不是标准
^1.2.3(caret)是 npm 风格,只有 uv 和 poetry 在 pyproject.toml 里支持。pip 原生不认 ^——如果你把 "fastapi^0.115" 写进给 pip 用的 requirements.txt,会报错。pip 路线要用 >=0.115,<1 表达同样的"锁定主版本"语义。
两种哲学:
- 钉死
==1.2.3:绝对可复现,但错过安全更新。适合"只求别变"的生产环境。 - 范围
>=1.2,<2/^1.2.3:允许兼容更新,配合锁文件(下节)兼顾"声明宽松 + 实际复现"。推荐。
2.3 锁定与复现:uv.lock vs requirements.txt¶
依赖声明(pyproject)写范围,但实际装的确切版本要靠锁文件固化,保证团队/CI 装出一致环境。
uv:uv.lock(自动、跨平台、含哈希)¶
uv.lock 记录整棵依赖树的确切版本 + 哈希,跨平台(Windows/Mac/Linux 都能复现)。提交到 git,团队/CI uv sync 即可。对照 Maven 的可复现构建。
pip:requirements.txt / pip-tools¶
pip 原生没有"锁文件"概念,靠 requirements.txt(扁平列表):
pip freeze 的问题:扁平(不区分直接/间接依赖)、无哈希(不防篡改)、跨平台可能不一致。
pip 路线的"uv.lock 替代":用 pip-tools:
pip install pip-tools
pip-compile # 读 pyproject 的依赖 → 生成 requirements.lock(含哈希、锁定)
pip-sync # 按 requirements.lock 精确同步环境
-
uv 锁定
-
pip 锁定(pip-tools)
2.4 可选依赖与 extras¶
把"开发/测试/文档"等非运行时依赖分开(对照 Maven 的 provided/test scope):
安装时按 extra 选:
常见模式
库发布到 PyPI 时,运行依赖放 dependencies(用户装库就装),测试/lint/文档放 optional-dependencies(用户按需)。这样用户 pip install yourlib 不会被你的 pytest 污染。
2.5 依赖来源:PyPI、镜像、Git、本地¶
默认从 PyPI 拉(对照 Maven Central)。国内访问慢时配镜像:
-
uv 镜像
或环境变量UV_INDEX_URL=...(注意:[[tool.uv.index]]是追加镜像、PyPI 仍在;UV_INDEX_URL是替换默认源) -
pip 镜像
其他来源:
uv add "git+https://github.com/encode/uvicorn" # uv:从 Git
uv add ../my-local-lib # 本地路径
pip install git+https://github.com/encode/uvicorn # pip:从 Git
2.6 依赖树与冲突排查¶
版本冲突(两个包要求同一依赖的不同版本)是常见痛点。
uv tree # uv:打印依赖树
pip show fastapi # pip:看某包的依赖
pip install pipdeptree && pipdeptree # pip:依赖树(第三方)
冲突时 uv 报 NoSolutionError、pip 报 ResolutionImpossible(来自 resolvelib)。解决:放宽约束、升级冲突包、或用 uv 的 overrides / 手动钉版本。
2.7 uv vs pip 深入对照¶
| 维度 | uv | pip |
|---|---|---|
| 解析器 | 现代PubGrub,强 | 较老,回溯式 |
^ caret 约束 |
✅ | ❌(用 >=,<) |
| 锁文件 | uv.lock(自动、跨平台、含哈希) |
无原生(pip freeze 或 pip-tools) |
| 速度 | 极快(Rust) | 慢 |
| 并行下载 | ✅ | 部分 |
| 全局缓存(硬链接共享) | ✅ | ❌ |
结论:新项目用 uv,老项目/受限环境用 pip + pip-tools 追平锁定能力。
本章练习¶
练习 2.1
解释 ^1.2.3、~=1.2.3、>=1.2.3,<2 三者的区别,以及哪个 pip 不支持。
参考答案
^1.2.3:>=1.2.3,<2.0.0(锁主版本 1,允许 minor/patch 更新)。pip 不支持(uv/poetry 专属)。~=1.2.3:>=1.2.3,==1.2.*(允许 patch 更新,锁到 1.2)。PEP 440,pip 支持。>=1.2.3,<2:显式范围,等价^1.2.3,pip 支持。 要在 pip 里表达 caret 语义,用>=1.2.3,<2。
练习 2.2
给一个库项目设计依赖:运行时依赖 fastapi(允许 0.115+)、开发依赖 pytest 和 ruff、文档依赖 mkdocs-material。写出 pyproject 片段和三种安装命令(uv / pip)。
练习 2.3
为什么直接提交 pip freeze 的 requirements.txt 不算真正的"锁定"?pip-tools 怎么补上?
参考答案
pip freeze 输出扁平列表,不区分直接/间接依赖、无哈希(不防篡改/不验证完整性)、跨平台可能不一致(如 Windows 装了 pywin32)。pip-compile(pip-tools)从 pyproject 的直接依赖出发,解析出完整锁定的 requirements.lock,含哈希,跨平台精确——更接近 uv.lock 的能力。
练习 2.4
国内拉包慢,分别给 uv 和 pip 配置清华镜像。
参考答案
uv:pyproject 加 [[tool.uv.index]] url = "https://pypi.tuna.tsinghua.edu.cn/simple",或设 UV_INDEX_URL。
pip:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple <pkg>,或写 ~/.pip/pip.conf([global] index-url = ...)永久生效。
上一章:进阶 1 · 环境与版本管理 | ← 回首页 | 下一章:进阶 3 · 配置与 .env