requirements.txt的历史定位

requirements.txt的诞生源于一个具体问题:如何在不同机器上复现相同的Python环境。这个文件本质上是pip install命令的批处理快捷方式,并非来自正式PEP规范,而是作为社区惯例逐渐形成的"意外标准"。

基本机制

文件格式简单直接:每行一个包声明。最常见的是==操作符精确锁定版本,如requests==2.26.0。支持的语法包括版本范围操作符(>=, <, ~=)、URL安装、本地路径、嵌套引用(-r)和约束文件(-c)。

pip freeze命令生成当前环境的完整包列表。问题在于它输出的是扁平化列表,包含所有直接依赖和传递性依赖,无法区分项目真正需要的顶层包和自动安装的依赖包。这导致依赖关系不透明,维护困难。

setup.py的角色混淆

setup.py通过install_requires参数声明抽象依赖,通常使用宽松的版本范围。这里存在一个核心区别:

  • 库(Library)应该声明宽松的版本范围,将版本选择权留给最终应用。如果库锁死版本,多个库依赖同一包的不同版本时会产生无解冲突。

  • 应用(Application)需要精确的版本锁定来保证部署一致性,要求一个包含所有依赖确切版本的具体列表。

这种"库用setup.py,应用用requirements.txt"的模式理论清晰,实践中却常被误用。

安全隐患:可执行代码问题

setup.py最根本的缺陷是它是可执行Python脚本。pip在安装源码发行版时,必须先执行setup.py才能获取包的元数据和依赖信息。这个"先有鸡还是先有蛋"的引导问题,导致了任意代码执行漏洞。

恶意代码可以嵌入setup.py,在pip install、甚至pip download过程中被执行。这是软件供应链攻击的关键入口点,也是推动打包体系现代化的主要动力。

pyproject.toml的标准化进程

pyproject.toml的出现是通过三个关键PEP逐步实现的架构性变革。

PEP 518:解决构建引导问题

2016年接受的PEP 518定义了[build-system]表,项目可以声明式地列出构建所需的依赖包和版本。这解决了引导问题:pip可以在不执行任何项目代码的情况下,在隔离环境中准备构建依赖。

[build-system]
requires = ["setuptools>=61.0", "wheel"]

PEP 517:前后端接口分离

PEP 517定义了构建前端(pip、build等)和构建后端(setuptools、hatchling等)的标准API。build-backend字段指定后端入口点:

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

这打破了setuptools的垄断,催生了竞争性的构建后端生态。

PEP 621:统一元数据声明

PEP 621标准化了[project]表,用于静态声明所有核心元数据:

[project]
name = "my-package"
version = "1.0.0"
description = "A package description"
dependencies = ["requests>=2.28.0"]

至此,setup.py对大多数项目已非必需。所有关键信息集中在一个静态、可解析的TOML文件中,实现了"单一事实来源"。

核心结构解析

[build-system]

  • requires:构建依赖列表,必须包含构建后端

  • build-backend:构建后端入口点路径

[project]

核心字段包括name、version、description、readme、requires-python、license、authors、classifiers等。

依赖声明:

  • dependencies:运行时抽象依赖数组

  • optional-dependencies:按场景分组的可选依赖(dev、test、docs等),支持通过pip install "package[dev]"安装

脚本与入口点:

  • [project.scripts]:命令行脚本,格式为script-name = "module:function"

  • [project.entry-points]:插件系统注册

动态元数据:

dynamic字段数组标记需要构建时计算的字段,常见用例是用setuptools-scm从Git标签生成版本号。

[tool]

为第三方工具提供统一配置命名空间,取代传统的点文件(.flake8、.isort.cfg等):

[tool.black]
line-length = 119

[tool.ruff]
select = ["E", "F", "I"]

[tool.pytest.ini_options]
testpaths = ["tests"]

抽象依赖与具体依赖的本质区别

pyproject.toml和requirements.txt的核心区别不是语法,而是哲学层面的目标差异:

抽象依赖(pyproject.toml)

定义项目需要什么,使用版本范围表达兼容性(如django>=4.2,<5.0)。目标是最大化兼容性,避免依赖冲突。服务于库的分发。

具体依赖(requirements.txt/锁文件)

精确定义所有包的确切版本(如django==4.2.4),包括所有传递性依赖。目标是锁定环境,确保可复现性。服务于应用的部署。

用例分离

  • 库开发:在pyproject.toml中声明宽松的抽象依赖,将版本选择权留给应用开发者

  • 应用开发:在pyproject.toml声明顶层依赖,用工具生成包含所有传递性依赖的锁文件,部署时使用锁文件安装

现代工作流模式

工作流:pyproject.toml → 工具 → 锁文件

  1. pyproject.toml作为单一事实来源:人类维护的唯一文件,声明项目意图

  2. 锁文件作为生成产物:机器生成,不应手动编辑,应提交到版本控制

方法A:pip-tools

# 从pyproject.toml生成锁文件
pip-compile -o requirements.txt pyproject.toml
pip-compile --extra dev -o requirements-dev.txt pyproject.toml

# 同步环境
pip-sync requirements-dev.txt

方法B:集成式工具(Poetry/PDM/Hatch)

# Poetry示例
poetry add requests  # 自动更新pyproject.toml和poetry.lock
poetry install       # 从锁文件安装

# 导出标准格式
poetry export -f requirements.txt --output requirements.txt

Docker集成

requirements.txt作为精确环境规约,是Docker构建过程中的理想输入:

COPY requirements.txt .
RUN pip install -r requirements.txt

setup.py到pyproject.toml的映射

pyproject.toml 字段 ([project] 表)setup.py setup() 参数setup.cfg [metadata] 或 [options] 键描述
namenamename包的发布名称,在PyPI上必须唯一
versionversionversion包的版本号。可以标记为dynamic以实现动态版本控制
descriptiondescriptiondescription项目的单行简短描述
readmelong_description, long_description_content_typelong_description指向包含详细描述的文件的路径(如README.md
requires-pythonpython_requirespython_requires指定项目兼容的Python版本范围,如">=3.8"
licenselicenselicense项目的许可证,推荐使用SPDX标识符
authors / maintainersauthor, author_email / maintainer, maintainer_emailauthor, author_email / maintainer, maintainer_email作者和维护者信息,以包含name和email的表数组形式提供
dependenciesinstall_requiresinstall_requires项目运行时抽象依赖列表
optional-dependenciesextras_requireoptions.extras_require可选依赖组,用于实现dev, test等附加功能
scriptsentry_points (用于 console_scripts)options.entry_points创建命令行可执行脚本
urlsurl, project_urlsproject_urls在PyPI上展示的额外链接,如项目主页、文档、源码仓库等
classifiersclassifiersclassifiersPyPI分类器列表,用于项目分类和搜索

构建后端生态

PEP 517解除了对setuptools的依赖,当前主要构建后端:

  • setuptools:历史最悠久,功能最全面,尤其适合包含C/C++扩展的项目

  • hatchling:现代设计,高度可扩展,通过插件系统支持自定义构建

  • pdm-backend:灵活性强,可委托其他后端处理特定任务(如C扩展编译)

  • flit-core:极简设计,适合纯Python包

  • maturin:专为Rust扩展设计,与PyO3深度集成

集成式工作流管理器对比

特性PoetryPDMHatchuv
主要焦点一体化项目管理一体化项目管理,支持PEP 582可扩展的环境/任务运行器高速安装器/解析器
PEP 621 兼容性部分(使用[tool.poetry]不适用(消费[project]
锁文件poetry.lockpdm.lock无原生锁文件(有插件)uv.lock(或生成requirements.txt)
环境管理管理自己的venv管理venv或使用PEP 582强大的环境矩阵venv创建与管理
实现语言PythonPythonPythonRust
独特功能成熟的生态系统支持PEP 582(无venv模式)类似tox的环境矩阵极致的速度

Rust重写的性能革命

Python工具链正在经历Rust重写的浪潮:

  • ruff:代码检查和格式化,比传统Python工具快10-100倍

  • uv:包管理器,依赖解析和安装速度比pip/Poetry快几个数量级

这些工具从根本上改变了开发者对工具性能的预期,大型项目的安装时间从数分钟缩短到几秒。

企业级安全考量

软件供应链攻击防御

哈希校验机制:

# 生成带哈希的锁文件
pip-compile --generate-hashes -o requirements.txt pyproject.toml

# 强制哈希校验安装
pip install --require-hashes -r requirements.txt

pip会验证每个包的SHA-256哈希值,不匹配则拒绝安装。这抵御中间人攻击和包篡改。

PEP 751(pylock.toml)将哈希校验从可选实践提升为强制要求。

私有包索引集成

企业使用私有索引(JFrog Artifactory、AWS CodeArtifact)用于:

  1. 缓存公共包,作为PyPI代理

  2. 托管内部专有包

关键安全配置:

  • --extra-index-url危险。同时查找PyPI和额外源,选择版本号更高的包。这是"依赖混淆"攻击的入口:攻击者可在公共PyPI发布同名但版本极高的恶意包。

  • --index-url安全。完全替换PyPI,只从指定索引查找。所有公共包通过私有索引的代理获取。

认证应使用有时效性的令牌,通过CI/CD秘密管理或环境变量提供。

标准化锁文件的未来

PEP 665失败的教训

早期锁文件标准提案PEP 665被拒绝,主要原因是不支持源码发行版(sdist),对科学计算社区不可接受。

PEP 751:pylock.toml

新标准吸取教训,定义了pylock.toml格式:

  • 明确支持sdist

  • 强制包含文件哈希值

  • 机器生成,跨工具互操作

未来场景:用PDM生成pylock.toml,用pip或uv安装,实现类似npm的package-lock.json或Cargo.lock的生态互操作性。

治理结构:PEP 772

Python打包委员会(Python Packaging Council)正在组建,提供比PyPA或Python指导委员会更专注、稳定的治理,确保打包标准的持续演进。

实践决策矩阵

库项目

  • 目标:最大兼容性

  • 配置:最小化pyproject.toml + 标准构建后端(hatchling/setuptools)

  • 原则:声明宽松版本范围,避免强加生态系统行为

应用项目

  • 目标:可复现部署

  • 推荐栈:

    • 速度优先:uv + pyproject.toml + venv
    • 一体化体验:Poetry/PDM
    • 多版本测试:Hatch
  • 流程:pyproject.toml声明意图 → 工具生成锁文件 → 部署使用锁文件

迁移路径

从旧系统迁移的步骤:

  1. 创建pyproject.toml,添加[build-system]

  2. 迁移所有静态元数据到[project]

  3. 动态元数据标记为dynamic保留setup.py中的计算逻辑

  4. 简化setup.py为最小垫片或完全移除

核心对比总结

特性/维度pyproject.tomlrequirements.txt
主要目的定义一个可分发的项目(库或应用)定义一个可复现的环境
依赖类型抽象的 (Abstract),通常是灵活的版本范围,如 requests>=2.20具体的 (Concrete),通常是固定的版本,如 requests==2.26.0
目标受众库的作者;应用的开发者(作为依赖声明的起点)部署脚本、CI/CD流水线、应用开发者(用于环境锁定)
编写者主要由人类手动编写和维护主要由机器(工具如 pip freeze, pip-compile)生成
内容范围综合性:项目元数据、构建系统配置、依赖、工具配置等专注性:仅包含Python包的安装列表
标准化由PEP 517, 518, 621等正式规范定义事实标准,格式较为松散,但被广泛接受
安全性声明式避免了setup.py的任意代码执行风险可通过--hash选项包含文件哈希,用于包完整性校验
在现代工作流中的角色单一事实来源 (Source of Truth),定义项目的意图生成的构建产物 (Generated Artifact),作为锁文件实现可复现性

演进主线与未来方向

Python打包生态的现代化转型呈现两条主线:

标准趋同

  • setup.py → pyproject.toml:从命令式到声明式

  • 碎片化配置 → [tool]表:统一工具配置命名空间

  • 各自为政的锁文件 → pylock.toml:标准化锁文件格式

  • 松散治理 → 打包委员会:制度化标准演进

工具加速

  • Python实现 → Rust实现:性能提升1-2个数量级

  • 代表性工具:ruff(linting)、uv(包管理)

  • 结果:实时linting、秒级依赖安装成为新常态

安全默认化

  • setup.py任意代码执行 → pyproject.toml静态解析

  • 可选哈希校验 → pylock.toml强制哈希

  • 私有索引最佳实践:使用--index-url而非--extra-index-url

最佳实践清单

  1. 新项目起点:以pyproject.toml为配置中心,选择合适的构建后端和工作流工具

  2. 依赖管理模式:人类维护pyproject.toml的抽象依赖,机器生成具体依赖锁文件

  3. 安全基线:启用哈希校验,正确配置私有索引,防御供应链攻击

  4. 工具选择:库项目保持最小化配置,应用项目采用集成式工具或uv

  5. 遗留项目逐步迁移setup.py内容到pyproject.toml,优先迁移静态元数据

Python打包的现代化不是简单的工具升级,而是从"可执行脚本驱动"到"声明式配置驱动"的范式转变,从"手动依赖管理"到"自动化锁文件生成"的工作流革新,从"事后安全加固"到"默认安全设计"的架构升级。这三重转变共同构成了一个更安全、更高效、更标准化的Python开发生态系统。

参考资料