Python的包管理把我坑惨了,原来pip和conda的区别这么大

简介: 本文以小许踩坑为例,深入剖析 conda 与 pip 的本质差异:conda 是跨语言环境管理器,统筹 Python、C 库、编译器等全栈依赖;pip 仅为 Python 包安装器,不感知底层二进制。混用导致“环境认知分裂”,引发隐性崩溃。推荐分层策略:conda 管底座,pip(仅纯 Python 包)补顶层,并通过 `environment.yml` 统一声明。

小许那天接手了一个离职同事的机器学习项目。README里只有一句话:“先 conda install 环境,缺什么再 pip install。”

他照着做,conda install 了主框架,又pip install了一个 PyPI 上才有的预处理库。一切正常,模型训练跑通,指标也复现了。他觉得自己运气不错。

两周后,他想给项目加一个新依赖。随手跑了一次 conda install,终端开始疯狂输出:

The following packages will be SUPERSEDED by a higher-priority channel:
 numpy        conda-forge/noarch  →  pkgs/main/linux-64
 ...

装完之后,import numpy 直接报 ImportError,提示二进制接口不兼容。他回滚了版本,又尝试 pip install,结果另一个之前正常的库也开始报错。

小许对着屏幕发愣:明明只是装了一个新包,为什么整个环境塌了?

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

同事留下的那句“先conda,再pip”,听起来像一个可以随意拼接的工作流。但pip和conda不是“两个可以互相替代的安装命令”,它们管理环境的方式从根上就不同。混着用,就是在两个互不知情的系统之间来回改同一套文件。

先看一个最直观的对比

同样装一个包,两者的行为完全不同。

pip install numpy

pip 从 PyPI 下载 numpy 的 wheel 包或源码包,装进当前激活的 Python 环境。如果当前没有激活任何虚拟环境,它就装进系统 Python。它不关心 numpy 底层依赖的 BLAS、LAPACK 这些 C/Fortran 数学库从哪来——那些必须由你的操作系统、或者某个预编译的 wheel 自己解决。

conda install numpy

conda 从配置的 channel(通常是 conda-forge)解析出一组二进制包,不仅包括 numpy 本身,还包括它依赖的 C 库、数学库、甚至 Python 解释器本身。所有组件被当作一个整体来求解,一起安装,一起记录。

用一个生活场景来理解。

pip 像一个只负责装家具的工人。你告诉他“把这张桌子搬进来”,他就搬桌子。桌子腿需要螺丝,螺丝需要扳手,这些工具从哪来,他不管——你自己得有,或者桌子出厂时恰好自带了一套适配你这个房间的螺丝。

conda 像一个整屋交付的装修队。你说“我要一个能工作的书房”,他不仅搬桌子,还把地板、电路、墙面、甚至房间本身的尺寸都一起考虑了。所有东西在进场之前就已经确认过彼此能兼容。

区别一目了然:一个只管上层组件,一个连底座一起管。

坑一:pip 不管非 Python 依赖,conda 管

这是两者最根本的分工差异。

pip 安装的是Python 包。如果这个包有 C 扩展,pip 会优先找预编译好的 wheel;找不到 wheel,它就回退到从源码编译,这时候你本机必须装有 C 编译器、对应的头文件、以及底层库。

pip install psycopg[c]   # 从源码编译,需要 PostgreSQL 客户端库和编译工具

conda 安装的是二进制包,包里已经包含了编译好的 C 扩展和它依赖的非 Python 库。你不需要本机有编译器,也不需要提前装好底层 C 库。

conda install -c conda-forge psycopg   # 二进制包,直接可用

这个差异在数据科学和机器学习场景下是决定性的。PyTorch 依赖 CUDA 工具链,NumPy/SciPy 依赖 MKL 或 OpenBLAS,GDAL 依赖一堆地理空间 C 库。用 pip 装这些包,你面对的可能是漫长的源码编译、缺失的头文件、以及和系统已有库的版本冲突。用 conda,这些底层依赖被打包在二进制里,conda 的求解器会把它们和 Python 包放在同一个解空间里一起匹配。

小许的项目里,conda install 维护的是一套包含底层数学库的 numpy 二进制;pip install 后来引入的包可能悄悄替换了 site-packages 里的某些文件。两者对“numpy 应该依赖哪个 BLAS”的假设不一致,ImportError 只是最后暴露出来的症状。

坑二:依赖解析的“视野”完全不同

pip 的依赖解析器(20.3 引入的回溯解析器)只看Python 包层面的版本约束。它会把 peach==1.0 和 apple==2.0 的依赖放在一起尝试,如果发现 peach 要求 banana==3.0、apple 要求 banana==2.0,它会报 ResolutionImpossible。这个机制本身是有效的,但它能看到的“世界”仅限于 PyPI 上的 Python 包。

conda 使用基于 libsolv 的 SAT 求解器(现代 conda 和 mamba 的默认方案)。它求解的变量不仅是 Python 包,还包括 Python 解释器版本、C 库、编译器运行时、CUDA 驱动等非 Python 组件。所有约束被翻译成逻辑子句,求解器寻找一组同时满足所有组件的赋值方案。

这个差异带来的后果是:pip 在它能看到的世界里可能是“一致”的,但在整个环境的真实状态下并不一致。 pip 不知道 conda 已经安装的 numpy 二进制链接的是哪个 BLAS,也不知道 conda 记录的 Python 版本和某个 C 扩展的 ABI 是否匹配。它在自己的视野里做最优解,但这个解可能和 conda 维护的底层状态冲突。

conda 的求解器虽然更“重”、更慢,但它的视野覆盖了环境里真正需要共存的所有组件。对于纯 Python 项目,这个优势不明显;对于有二进制依赖的栈,它是环境能站住的前提。

坑三:混用会制造“环境认知分裂”

这是小许遇到的核心问题,也是 conda 官方反复警告的“environmental damage”。

conda 在安装包时,会把每一个已安装包的信息写进环境里的 conda-meta 目录。这是 conda 对“这个环境里到底有什么”的唯一记录。下次 conda install 或 conda update 时,求解器读取的是这份记录,而不是去扫描 site-packages 里的实际文件。

pip 安装包时,**不会更新 conda-meta**。它只把文件写进 site-packages,然后留下自己的 *.dist-info 元数据。对 conda 来说,这些 pip 装进来的包是“不存在的”。

于是环境出现了两个互相矛盾的“事实”:

  • conda 的认知:环境里只有我记录的那些包,版本是 X。
  • 磁盘上的现实:site-packages 里的文件已经被 pip 改过,某些包的版本变成了 Y。

当 conda 下一次尝试修改环境时,它基于一份过时的认知做决策。它可能认为某个依赖关系仍然成立,但实际上 pip 已经把它破坏了。错误可能延迟几天甚至几个月,在你安装、更新或删除一个看起来完全无关的包时突然爆发。

小许的 conda install 触发了重新求解,而 conda 的求解依据是一份没有反映 pip 改动的记录。结果就是“SUPERSEDED”——conda 按自己的认知覆盖了 pip 留下的文件,底层二进制被换成了一个和现有 Python ABI 不匹配的版本。

坑四:环境隔离的“重量”不同,但隔离本身不是免费的

pip 本身不创建环境。它依赖 venv 或 virtualenv 来提供隔离。python -m venv .venv 创建的环境很轻,本质上是独立的 site-packages 目录加上指向系统 Python 解释器的链接。磁盘占用小,启动快,但隔离的边界仅限于 Python 包层面。系统层面的 C 库、CUDA 驱动、Python 解释器版本,仍然共享。

conda 的环境是内置的、更重的隔离。每个 conda 环境可以拥有自己的 Python 解释器、自己的 C 库、自己的二进制工具链。conda 通过包缓存和硬链接来降低磁盘开销:同一个包在多个环境之间共享时,文件只有一份,环境目录里只是硬链接。但这种共享依赖文件系统支持硬链接,跨设备或跨文件系统时会退化为复制,环境体积会显著增加。

隔离的“重”本身不是缺点。对于需要固定 CUDA 版本、固定 Python ABI、固定 BLAS 实现的项目,这种重量级隔离是必要的。问题在于:pip 在一个 conda 环境里安装时,它安装的是“Python 包层面的隔离”,并不尊重 conda 环境在二进制层面的边界。 你以为你在同一个隔离环境里操作,实际上两个工具对“隔离环境”的定义并不一致。

那到底该怎么选?

判断标准不是“哪个更好”,而是“你的依赖里有没有非 Python 的东西”。

纯 Python 项目,依赖都能在 PyPI 上以 wheel 形式拿到,不涉及 C 扩展、CUDA、系统级库:pip + venv 是标准选择。轻量,和 Python 官方工具链一致,Docker 镜像小,部署简单。

数据科学、机器学习、科学计算,依赖链里有 BLAS、CUDA、GDAL、R 互操作、或需要特定编译器运行时:conda(或者更快的 mamba)是更省心的选择。它把底层二进制和 Python 包放在同一个求解空间里,避免了 pip 层面无法解决的版本漂移。

混合场景,conda 环境里确实需要某个只在 PyPI 上有的纯 Python 包:可以混用,但必须遵守顺序和边界。先 conda 装完所有 conda 能提供的包,最后再用 pip 补纯 Python 的缺口。安装 pip 包时用 python -m pip,确保装进当前激活的 conda 环境,而不是别的 Python 解释器。同一个包绝对不要既用 conda 装又用 pip 装——那是冲突最高发的区域。

需要诚实地说:即使遵守了“先 conda 后 pip”,混用仍然是一种脆弱的、手工维护的平衡。conda 社区正在推进 conda-pypi 项目(conda 26.5.0 起可选),让 conda 的求解器原生理解 PyPI 上的纯 Python wheel,从而把 pip 这一步纳入同一个求解过程,不再制造“conda-meta 不知道的包”。在这个方案成熟之前,混用的最佳实践本质上是尽量缩小 pip 的使用范围,并且接受环境可能需要在某个时刻重建。

回到小许的项目

小许最终的解决方案不是继续修补那个环境,而是重建。

他把项目依赖分成两层:conda 能提供的(numpy、pytorch、以及它们的底层二进制依赖)全部写进 environment.yml;只在 PyPI 上有的纯 Python 包,作为 pip: 节放在 environment.yml 的最后,让 conda 先建完底座,再由 pip 补最上层的纯 Python 部分。

name: ml-project
dependencies:
 - python=3.11
 - numpy
 - pytorch
 - pip
 - pip:
   - some-pypi-only-package

他后来在文档里补了一条:“conda 管的是一整个房间,pip 管的是房间里的一件家具。你可以往conda装好的房间里再搬一件pip的家具,但不要让pip去动房间的地板和电路。 如果某个包conda有,就用conda;只有conda确实没有、而且确认是纯Python包时,才用pip,并且放在最后装。”

pip 和 conda 的区别,不在于谁“更强”。它们解决的是不同层级的问题。pip 是 Python 包安装器,conda 是跨语言的环境管理器。把环境管理器的工作交给包安装器,或者反过来,就会得到小许那种“能跑,但不知道什么时候会塌”的环境。

目录
相关文章
|
6月前
|
人工智能 前端开发 Shell
一个文件让 AI Coding 效率翻倍:AGENTS.md 实践指南
文章内容基于作者个人技术实践与独立思考,旨在分享经验,仅代表个人观点。
15755 4
一个文件让 AI Coding 效率翻倍:AGENTS.md 实践指南
|
9天前
|
JSON 缓存 安全
Python的序列化把我坑惨了,原来json和pickle的区别这么大
本文通过小冯的缓存开发实践,对比JSON与Pickle的核心差异:JSON是跨语言、安全、人类可读的标准文本格式,但仅支持基本类型;Pickle是Python专属二进制格式,可完整保留对象类型与结构,却存在跨语言不可用、版本兼容性差及严重安全风险(可执行任意代码)。
50 0
|
2月前
|
数据采集 JavaScript 前端开发
房产数据对比爬虫:同时爬取链家+贝壳+安居客,做房价横向对比
本文详解如何突破链家/贝壳(JS渲染+字体反爬)与安居客(动态Token+请求头校验)的差异化反爬机制,基于适配器模式构建跨平台爬虫架构,结合站大爷隧道代理实现IP隔离,并通过小区名+户型+面积模糊匹配,完成房价横向对比分析,助力识别虚高挂牌与低价引流。
452 1
|
24天前
|
缓存 安全 Java
Python的集合运算把我坑惨了,原来set和frozenset的区别这么大
本文通过小王踩坑的故事,详解 Python 中 set 与 frozenset 的核心区别:set 可变、不可哈希,适用于动态修改;frozenset 不可变、可哈希,可用于字典键、嵌套集合等场景。厘清二者语义差异,避开常见陷阱。
88 1
|
2天前
|
数据采集 BI 数据库
Python的主入口把我坑惨了,原来__name__和__main__的区别这么大
本文通过小周的数据清洗脚本案例,深入浅出地讲解 Python 中 `if __name__ == "__main__":` 的原理与必要性:`__name__` 在直接运行时为 `"__main__"`,被导入时为模块名;该判断可防止测试代码意外执行,避免多进程重复加载等问题,是模块复用与脚本独立运行的关键机制。(239字)
30 0
|
6天前
|
JSON 缓存 运维
Python的导入把我坑惨了,原来import和from import的区别这么大
本文通过电商配置模块的典型故障,深入剖析Python中`import module`与`from module import name`的本质区别:前者绑定模块对象(实时访问),后者绑定导入时刻的值(静态快照)。揭示了调试开关不生效、循环导入报错、命名污染等五大常见陷阱,并给出清晰的选型指南。
48 0
|
7天前
|
运维 安全 算法
Python的断言把我坑惨了,原来assert和raise的区别这么大
本文通过小许因误用`assert`做权限校验导致生产环境安全漏洞的案例,深入剖析`assert`与`raise`的本质区别:前者是开发自检工具,启用`-O`优化时会被完全移除;后者是正式错误处理机制,始终生效。强调业务逻辑(权限、参数、外部数据校验)必须用`raise`,不可依赖`assert`。
50 0
|
8天前
|
数据采集 网络协议 Java
图片爬虫进阶:从单线程到协程,性能提升100倍的优化之路
本文详解图片爬虫性能优化路径:从单线程(40分钟/1000张)到协程(20秒),实测提速超100倍。通过对比单线程、多线程、线程池与异步协程,揭示IO密集型任务“等待时间重叠”的并发本质,并结合站大爷隧道代理应对IP频率限制,兼顾速度与稳定性。
45 0
|
14天前
|
缓存 计算机视觉 开发者
Python的字节序列把我坑惨了,原来bytes和bytearray的区别这么大
本文详解Python中`bytes`与`bytearray`的核心区别:前者不可变,适合只读、哈希场景;后者可变,支持索引赋值、追加等操作,适用于协议解析、封包等需频繁修改二进制数据的场合。附常见坑点与性能对比。(239字)
53 0
|
15天前
|
JSON 数据库 数据格式
Python的字符串格式化把我坑惨了,原来f-string和format的区别这么大
本文通过小周开发通知模板系统的实际案例,深入剖析f-string与format的核心区别:f-string是“定义时求值”的字面量,适用于代码中写死的模板;format是“调用时求值”的方法,专为运行时动态模板(如数据库读取)而生。厘清这一本质,可避免常见坑点。
99 0