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 → 工具 → 锁文件
pyproject.toml作为单一事实来源:人类维护的唯一文件,声明项目意图
锁文件作为生成产物:机器生成,不应手动编辑,应提交到版本控制
方法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] 键 | 描述 |
---|---|---|---|
name | name | name | 包的发布名称,在PyPI上必须唯一 |
version | version | version | 包的版本号。可以标记为dynamic以实现动态版本控制 |
description | description | description | 项目的单行简短描述 |
readme | long_description, long_description_content_type | long_description | 指向包含详细描述的文件的路径(如README.md) |
requires-python | python_requires | python_requires | 指定项目兼容的Python版本范围,如">=3.8" |
license | license | license | 项目的许可证,推荐使用SPDX标识符 |
authors / maintainers | author, author_email / maintainer, maintainer_email | author, author_email / maintainer, maintainer_email | 作者和维护者信息,以包含name和email的表数组形式提供 |
dependencies | install_requires | install_requires | 项目运行时抽象依赖列表 |
optional-dependencies | extras_require | options.extras_require | 可选依赖组,用于实现dev, test等附加功能 |
scripts | entry_points (用于 console_scripts) | options.entry_points | 创建命令行可执行脚本 |
urls | url, project_urls | project_urls | 在PyPI上展示的额外链接,如项目主页、文档、源码仓库等 |
classifiers | classifiers | classifiers | PyPI分类器列表,用于项目分类和搜索 |
构建后端生态
PEP 517解除了对setuptools的依赖,当前主要构建后端:
setuptools:历史最悠久,功能最全面,尤其适合包含C/C++扩展的项目
hatchling:现代设计,高度可扩展,通过插件系统支持自定义构建
pdm-backend:灵活性强,可委托其他后端处理特定任务(如C扩展编译)
flit-core:极简设计,适合纯Python包
maturin:专为Rust扩展设计,与PyO3深度集成
集成式工作流管理器对比
特性 | Poetry | PDM | Hatch | uv |
---|---|---|---|---|
主要焦点 | 一体化项目管理 | 一体化项目管理,支持PEP 582 | 可扩展的环境/任务运行器 | 高速安装器/解析器 |
PEP 621 兼容性 | 部分(使用[tool.poetry] ) | 是 | 是 | 不适用(消费[project] ) |
锁文件 | poetry.lock | pdm.lock | 无原生锁文件(有插件) | uv.lock(或生成requirements.txt) |
环境管理 | 管理自己的venv | 管理venv或使用PEP 582 | 强大的环境矩阵 | venv创建与管理 |
实现语言 | Python | Python | Python | Rust |
独特功能 | 成熟的生态系统 | 支持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)用于:
缓存公共包,作为PyPI代理
托管内部专有包
关键安全配置:
--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声明意图 → 工具生成锁文件 → 部署使用锁文件
迁移路径
从旧系统迁移的步骤:
创建pyproject.toml,添加
[build-system]
表迁移所有静态元数据到
[project]
表动态元数据标记为
dynamic
,保留setup.py中的计算逻辑简化setup.py为最小垫片或完全移除
核心对比总结
特性/维度 | pyproject.toml | requirements.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
最佳实践清单
新项目起点:以pyproject.toml为配置中心,选择合适的构建后端和工作流工具
依赖管理模式:人类维护pyproject.toml的抽象依赖,机器生成具体依赖锁文件
安全基线:启用哈希校验,正确配置私有索引,防御供应链攻击
工具选择:库项目保持最小化配置,应用项目采用集成式工具或uv
遗留项目:逐步迁移setup.py内容到pyproject.toml,优先迁移静态元数据
Python打包的现代化不是简单的工具升级,而是从"可执行脚本驱动"到"声明式配置驱动"的范式转变,从"手动依赖管理"到"自动化锁文件生成"的工作流革新,从"事后安全加固"到"默认安全设计"的架构升级。这三重转变共同构成了一个更安全、更高效、更标准化的Python开发生态系统。