Python的pip依赖把我折腾惨了,原来requirements.txt和poetry能打出火星撞地球

简介: 本文揭秘Python依赖管理的“火星撞地球”现象:`requirements.txt`仅记录直接依赖,无冲突检测;Poetry则通过`pyproject.toml`+`poetry.lock`实现全依赖树解析与锁定,保障环境一致性。二者哲学迥异,混用必出问题。

讲个真事儿。

上个月我接手了一个维护项目,代码仓库里躺着两个文件:requirements.txt和pyproject.toml。两个文件都在,而且都在用。我一开始没觉得有什么问题,反正都是管依赖的嘛,各管各的呗。

结果我运行poetry install装完依赖,再跑测试——全红了。我又用pip install -r requirements.txt重装了一遍,测试又全绿了。更诡异的是,我把两个文件里的依赖版本逐一比对,发现它们写的版本号一模一样。

一模一样的版本号,装出来的环境却不一样。

那天下午我花了四个小时才搞明白一件事:requirements.txt和Poetry,看起来都在管依赖,但它们对“依赖”这两个字的理解完全不同。 当两个工具同时出现在一个项目里的时候,打的不是架,是“火星撞地球”。

代理 IP 使用小技巧 让你的数据抓取效率翻倍 (80).png

先搞清楚 requirements.txt 到底是什么

requirements.txt本质上就是一个文本文件,一行一个包名。你可以写成这样:

requests
flask
numpy

也可以写成这样:

requests==2.31.0
flask>=2.0.0
numpy~=1.24.0

它没有什么复杂的结构,就是一个清单——你要什么包,我就记什么包。

但问题来了:requirements.txt只记录你直接依赖的包,不记录这些包自己又依赖了什么。

你装requests的时候,requests会顺带装上urllib3、certifi、charset-normalizer等好几个包。这些“间接依赖”在requirements.txt里是没有的。除非你用pip freeze把它们全部导出来。

pip freeze导出的是当前环境里所有已安装的包——包括直接依赖和所有间接依赖。这看起来解决了问题,但带来了新麻烦:你分不清哪些是你主动要装的,哪些是别人捎带脚装上的。而且如果不同的人在不同的时间执行pip freeze,由于PyPI上的包版本在变,生成的requirements.txt可能完全不同。

更麻烦的是,pip没有内置的依赖冲突解析器。假设包A需要urllib3的1.x版本,包B需要2.x版本。pip不会提前告诉你“这两个不兼容”,它会直接安装——装成什么样取决于安装顺序。可能这次跑得好好的,下次就因为版本变了莫名其妙地挂了。

这就是为什么很多Python项目会出现“在我机器上能跑”的经典问题。

再说 Poetry 是怎么做的

Poetry的核心理念是:依赖管理不应该是个事后补丁。

它用两个文件来管依赖:

pyproject.toml —— 你告诉Poetry你想要什么。比如:

[tool.poetry.dependencies]

python = "^3.9"

requests = "^2.31.0"

flask = ">=2.0.0"

这里的^2.31.0意思是“兼容2.31.0的版本”,可以自动升级到2.31.x但不能升到3.0.0。你写的都是“版本约束”,不是“精确版本”。

poetry.lock —— Poetry帮你算出来的精确版本。它记录了每一个包——包括所有间接依赖——的精确版本号、哈希值等。这个文件是自动生成的,你不应该手动编辑它。

关键区别在于:Poetry会在安装之前先做依赖解析。它把整个依赖树都算一遍,确认所有版本都能兼容,然后才动手安装。如果发现冲突,它会直接告诉你“这几个包不兼容”,而不是先把东西装上再说。

而且Poetry会自动为每个项目创建独立的虚拟环境,你不用再手动python -m venv和source activate。

火星撞地球是怎么发生的

回到我遇到的那个项目。

requirements.txt是手动维护的,里面只列了五六个人直接依赖的包。而pyproject.toml是Poetry生成的,它记录了完整的依赖树。

表面上看两个文件里的版本号一样,但实际上:

requirements.txt里写的是requests==2.31.0,但没写它依赖的urllib3是什么版本。而poetry.lock里锁定了urllib3==1.26.18。

问题出在另一个间接依赖上。项目里还用了一个叫botocore的包,它要求urllib3的版本小于2.0。pip install -r requirements.txt的时候,pip先装了requests==2.31.0,它默认装的是urllib3的最新版——当时已经是2.x了。然后装botocore的时候,发现urllib3版本不对,pip的处理方式是悄悄把urllib3降级到1.x。装是装上了,但requests和urllib3的搭配变了。

而Poetry不一样。它在安装之前就把整棵树算了一遍,发现requests==2.31.0和botocore对urllib3的版本要求有冲突,直接报错终止,让你去解决。

一个工具悄悄帮你“解决”了问题(但其实没解决),另一个工具拒绝帮你“解决”问题(但保证了环境的一致性)。

这就是两个工具放在一起用的后果——你以为它们在做同一件事,实际上它们的哲学完全相反。

那到底该用哪个?

如果你只有一个简单的脚本,或者是一个一次性任务,用pip加requirements.txt完全够用。轻量、直接、不需要学新东西。

但如果你的项目有以下任何特征,建议认真考虑Poetry:

  • 有多个开发者协作 —— 每个人装出来的环境必须一致
  • 有CI/CD流程 —— 构建环境必须可复现
  • 依赖超过10个包 —— 间接依赖的冲突风险急剧上升
  • 要打包发布 —— Poetry内置了构建和发布功能
特性 requirements.txt Poetry
配置文件 requirements.txt pyproject.toml + poetry.lock
记录内容 直接依赖(或全部已装包) 直接依赖 + 完整依赖树
依赖解析 安装时,无冲突检测 安装前,自动解决冲突
环境隔离 手动配合venv 自动创建和管理
开发/生产分离 不支持 支持分组
学习成本 低 中等

几个常见误区

误区一:“我用pip freeze导出了所有包,就跟Poetry的lock文件一样了。”

不一样。pip freeze导出的只是当前环境的一个快照,它没有记录“这些包之间的依赖关系是什么”。Poetry的poetry.lock记录的是完整的依赖树结构,而且带有哈希校验,能防止安装过程中被篡改。

误区二:“我把requirements.txt和pyproject.toml都留着,两边都兼容。”

这正是我踩的坑。两个工具同时存在,迟早会出问题。选一个,删掉另一个。

误区三:“Poetry太慢了,不如pip快。”

Poetry在依赖解析阶段确实比pip慢,因为它要做完整的依赖树计算。但这个“慢”换来的是“准”——它确保装出来的环境是对的。而且这个解析只在第一次安装或更新依赖时做一次,日常开发中影响不大。

如果你非要用requirements.txt

有些团队暂时无法切换到Poetry,或者项目已经用requirements.txt跑了很多年。这种情况下,至少做到这几点:

  1. 永远锁定精确版本 —— 用==而不是>=或~=
  2. 区分两个文件 —— requirements.in记录直接依赖(人工维护),requirements.txt记录完整锁定(自动生成)
  3. 在干净环境中重新生成 —— 每次更新依赖时,在新虚拟环境里执行pip freeze
  4. 考虑pip-tools —— 它是一个轻量级方案,能帮你自动生成带完整依赖树的requirements.txt

一句话总结

requirements.txt是一个“购物清单”——你想买什么写什么;Poetry是一个“采购系统”——你说你想要什么,它帮你算好买什么、怎么搭配、会不会打架,然后一次性搞定。

两个工具没有绝对的好坏,但它们服务的是不同阶段的项目。小脚本用requirements.txt,正经项目用Poetry。别两个一起用,别让它们在一个项目里打起来。

目录
相关文章
|
16天前
|
缓存 Java 测试技术
Python的默认参数把我坑惨了,原来写[]和写None的区别这么大
Python默认参数在函数定义时求值一次,若用可变对象(如[]、{})作默认值,会导致多次调用共享同一对象,引发静默累积bug。正确做法是用None作默认值,在函数内初始化。
66 0
|
29天前
|
算法 安全 C++
Python的类属性把我带进沟里了,原来实例查找和类查找顺序这么反直觉
本文以真实踩坑案例切入,深入剖析Python类属性与实例属性的核心区别:查找时“先实例后类”,但赋值与修改行为截然不同。重点揭示可变类属性(如列表、字典)引发的共享陷阱,并用“公共储物柜vs私人抽屉”生动比喻助你彻底理解。
84 0
|
1月前
|
存储 JSON 数据格式
Python的生成器把我内存救活了,原来return和yield差距这么大
本文通过真实案例对比`return`与`yield`:前者一次性加载50万条JSON数据致内存飙至14GB,后者用生成器逐行处理仅耗80MB。深入剖析二者在执行机制、内存模型及编程思维(批处理vs流式)的本质差异,并详解生成器的惰性求值、状态保持、一次性等特性及典型应用场景。
89 0
|
1月前
|
数据采集 安全 JavaScript
Python的默认参数把我坑惨了,原来可变对象当默认值等于给自己埋雷
本文详解Python经典陷阱:可变对象作默认参数。因默认值在函数定义时仅创建一次,`def f(lst=[])` 中的列表会被所有调用共享,导致数据意外累积、污染。推荐统一用 `None` 占位,在函数内初始化,避免隐蔽bug。
107 0
|
1月前
|
TensorFlow 算法框架/工具 Python
Python的虚拟环境把我整不会了,原来venv和conda背后藏着这么多坑
本文通过实习生踩坑经历,深入对比Python虚拟环境工具venv与conda的核心差异:venv轻量专注包隔离,但绑定Python版本;conda全能自包含,支持多版本及系统级依赖。二者逻辑迥异,混用易致环境崩溃。文末给出选型指南与Poetry+pyenv现代方案建议。(239字)
199 0
|
1月前
|
数据库连接 测试技术 数据库
Python的异常处理把我搞怕了,原来try放在循环内外差别这么大
本文通过真实数据迁移案例,深入剖析`try`语句在循环内外的语义差异:循环内实现容错处理(单条失败不影响整体),循环外保障事务原子性(全成功或全回滚)。结合性能、作用域、错误定位等维度,提供三问决策法,并介绍嵌套try、else/finally等进阶用法,助你写出语义清晰、健壮可靠的代码。(239字)
101 0
|
1月前
|
存储 大数据 数据处理
Python的列表推导式把我写崩了,原来圆括号和方括号不是一回事
本文通过真实案例揭示Python中列表推导式(方括号)与生成器表达式(圆括号)的本质区别:前者一次性加载全部数据,内存占用大;后者惰性求值,内存恒定约100字节。2.8GB日志处理中,仅改括号就使内存从4.2GB降至50MB。核心口诀:“方括号——全部给我;圆括号——要一个给一个”。
136 0
|
1月前
|
C++ Python
Python的装饰器把我绕晕了,原来带参和不带参区别这么大
本文深入剖析Python装饰器加括号与不加括号的本质区别:`@decorator`是直接用函数本身作装饰器;`@decorator()`则是先调用函数,用其返回值作装饰器。通过真实踩坑案例,厘清无参/带参装饰器的执行逻辑、类实现方式及使用规范,助你彻底掌握装饰器核心原理。(239字)
65 0
|
1月前
|
缓存 运维 安全
一直以为deepcopy很安全,直到线上数据全乱了才幡然醒悟
一篇真实技术复盘:程序员用 `deepcopy` 生成订单快照,却因共享引用、性能瓶颈与不可拷贝对象引发线上数据错乱。文章详解四大深坑及安全替代方案,提醒开发者——“安全”不等于盲目复制。(239字)
95 0
|
1月前
|
数据采集 安全 Java
从入门到放弃,Python的GIL锁比我想象的还要‘坑’
本文以亲身踩坑经历切入,揭秘Python多线程“越优化越慢”的真相——GIL(全局解释器锁):它保障了单核时代的内存安全,却制约了多核下CPU密集型任务的并行;I/O密集型任务仍可高效并发。文章厘清误解、剖析原理,并给出多进程、异步、C扩展及Python 3.13自由线程等实用绕过方案。(239字)
219 0