跳转至

进阶 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

    <dependencies>
      <dependency>
        <groupId>io.fastjson</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.83</version>
      </dependency>
    </dependencies>
    
  • Python (pyproject)

    dependencies = ["fastapi>=0.115"]
    

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 和 poetrypyproject.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 add fastapi          # 自动解析依赖、更新 pyproject + uv.lock
uv sync                 # 按 uv.lock 精确复现

uv.lock 记录整棵依赖树的确切版本 + 哈希,跨平台(Windows/Mac/Linux 都能复现)。提交到 git,团队/CI uv sync 即可。对照 Maven 的可复现构建。

pip:requirements.txt / pip-tools

pip 原生没有"锁文件"概念,靠 requirements.txt(扁平列表):

pip freeze > requirements.txt          # 冻结当前环境(扁平、无哈希)
pip install -r requirements.txt        # 复现

pip freeze 的问题:扁平(不区分直接/间接依赖)、无哈希(不防篡改)、跨平台可能不一致。

pip 路线的"uv.lock 替代":用 pip-tools

pip install pip-tools
pip-compile              # 读 pyproject 的依赖 → 生成 requirements.lock(含哈希、锁定)
pip-sync                 # 按 requirements.lock 精确同步环境
  • uv 锁定

    # pyproject.toml(声明,宽松)
    dependencies = ["fastapi>=0.115"]
    # uv.lock(锁定,精确,自动生成)
    
  • pip 锁定(pip-tools)

    # requirements.in(声明,宽松)
    fastapi>=0.115
    # requirements.lock(锁定,pip-compile 生成)
    fastapi==0.115.3
    

2.4 可选依赖与 extras

把"开发/测试/文档"等非运行时依赖分开(对照 Maven 的 provided/test scope):

[project.optional-dependencies]
dev = ["pytest>=8", "ruff>=0.5"]
docs = ["mkdocs-material>=9.6"]

安装时按 extra 选:

uv sync --extra dev              # uv:装 dev 这组
pip install -e ".[dev]"          # pip:方括号语法装 extras

常见模式

库发布到 PyPI 时,运行依赖放 dependencies(用户装库就装),测试/lint/文档放 optional-dependencies(用户按需)。这样用户 pip install yourlib 不会被你的 pytest 污染。


2.5 依赖来源:PyPI、镜像、Git、本地

默认从 PyPI 拉(对照 Maven Central)。国内访问慢时配镜像:

  • uv 镜像

    # pyproject.toml
    [[tool.uv.index]]
    url = "https://pypi.tuna.tsinghua.edu.cn/simple"
    
    或环境变量 UV_INDEX_URL=...(注意:[[tool.uv.index]]追加镜像、PyPI 仍在;UV_INDEX_URL替换默认源)

  • pip 镜像

    pip install -i https://pypi.tuna.tsinghua.edu.cn/simple fastapi
    # 或写进 ~/.pip/pip.conf 永久生效
    

其他来源:

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 freezepip-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+)、开发依赖 pytestruff、文档依赖 mkdocs-material。写出 pyproject 片段和三种安装命令(uv / pip)。

参考答案

[project]
dependencies = ["fastapi>=0.115"]
[project.optional-dependencies]
dev = ["pytest>=8", "ruff>=0.5"]
docs = ["mkdocs-material>=9.6"]
uv sync --extra dev          # 或 --all-extras
pip install -e ".[dev]"
pip install -e ".[dev,docs]"

练习 2.3

为什么直接提交 pip freezerequirements.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