一、依赖管理基础:什么是依赖、依赖冲突的产生原因、Python包管理工具pip简介

1.1 什么是依赖?说白了就是“你离不开它”

依赖这个概念,其实特别好理解。你写一个Python程序,想发个HTTP请求,总不能自己从socket开始写吧?你会用requests库。想处理JSON数据?用内置的json模块就行。想做个数据分析?那pandas、numpy就来了。

这些你“离不开”的第三方库,就是依赖。

我个人习惯把依赖分成两类:

  • 直接依赖:你代码里明确import的库。比如你写了个爬虫,import了requests和beautifulsoup4,这俩就是直接依赖。
  • 间接依赖:你依赖的库,它自己也有依赖。比如requests底层依赖urllib3,urllib3又依赖certifi。这些你根本没import过,但少了它们程序就跑不起来。

我在项目中遇到过最离谱的一次,一个简单的Flask应用,装完一看,依赖树里躺了47个包。你想想看,你只写了3个import,背后却拖了44个“小弟”。这就是依赖传递的威力。

一句话总结:依赖就是你的代码“请来帮忙”的其他代码。你请的人越多,管理起来就越复杂。

1.2 依赖冲突:为什么好好的项目说崩就崩?

依赖冲突,说白了就是“版本打架”。

场景是这样的:你的项目A依赖包B(要求版本≥1.0),同时也依赖包C(要求版本≥2.0)。但包C的2.0版本,它内部依赖包B的版本必须是≤0.9。这下好了——包B到底该装1.0还是0.9?装哪个都会得罪另一方。

这就是依赖冲突的典型场景。我总结了几种常见原因:

冲突类型 产生原因 真实案例(我踩过的坑)
版本范围不兼容 两个依赖对同一个间接依赖的版本要求矛盾 曾经同时用tensorflow 2.4和torch 1.8,它们对numpy的版本要求打架
API变更 依赖升级后,函数签名或行为变了 requests从1.x升到2.x时,response.json的行为变了
循环依赖 包A依赖包B,包B又依赖包A Python里虽然少见,但我在处理一个内部工具库时遇到过
锁定文件过期 requirements.txt锁死了版本,但新环境装不上 有一次部署到新服务器,发现pyyaml 5.4已经下架了

⚠️ 避坑指南:我曾经在项目上线前一天,因为升级了一个小版本导致整个CI流水线全红。从那以后,我养成了一个习惯——永远不要在生产环境直接升级依赖。先在测试环境跑一遍,确认没问题再动。

1.3 pip:Python世界的“快递小哥”

说到Python包管理,pip是绕不开的工具。它全称是“Pip Installs Packages”,说白了就是帮你下载、安装、管理Python包的工具。

我刚开始用Python时,觉得pip就是个装包的工具,没什么好学的。后来被坑了几次才发现,这里面的门道还真不少。

pip的核心功能

  • 安装包:pip install requests,一行命令搞定
  • 卸载包:pip uninstall requests,干净利落
  • 查看已安装的包:pip list,看看你的环境里都装了啥
  • 导出依赖列表:pip freeze > requirements.txt,把当前环境的依赖快照保存下来
  • 从文件安装:pip install -r requirements.txt,一键还原依赖环境

pip的常用参数

参数 作用 示例
--upgrade 升级到最新版本 pip install --upgrade requests
--user 安装到用户目录(避免污染系统Python) pip install --user requests
-i 指定镜像源(国内用户必备) pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests
--no-deps 只安装包本身,不装依赖 pip install --no-deps requests

💡 我的小技巧:在国内用pip,一定要配镜像源。我习惯在~/.pip/pip.conf里写上:

[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
trusted-host = pypi.tuna.tsinghua.edu.cn

这样就不用每次手动加-i参数了。省事不少。

pip的依赖解析机制

pip是怎么决定装哪个版本的?嗯,这里有个小故事。

早期版本的pip,依赖解析能力很弱。你执行pip install A B,它会先装A,再装B。如果A和B对同一个依赖有冲突,pip会直接报错,或者更糟——悄悄覆盖掉已经装好的版本。

我记得有一次,我在一个项目里同时用了flask和django(别问为什么,历史遗留问题),pip装完以后,flask跑不起来了。查了半天,发现是click这个库被django的依赖覆盖成了不兼容的版本。

从pip 20.3开始,引入了新的依赖解析器(resolver),它会先分析所有依赖的版本要求,找到一个“皆大欢喜”的方案,再开始安装。如果找不到,就直接报错,告诉你哪里冲突了。

这个改进很好,但也带来一个问题——解析时间变长了。尤其是依赖树很深的时候,pip可能要花几十秒甚至几分钟来算版本。

核心要点:pip的依赖解析是“贪心”的——它倾向于安装最新版本,除非你明确指定了版本范围。这也是很多冲突的根源。所以我的建议是:永远在requirements.txt里锁定版本号,不要用>=这种宽松写法。

1.4 一个小实验:看看你的依赖树

光说不练假把式。我建议你现在就打开终端,试试这个:

# 创建一个干净的虚拟环境
python -m venv test_env
source test_env/bin/activate  # Windows用 test_env\Scripts\activate

# 安装一个包
pip install flask

# 看看都装了啥
pip list

# 看看依赖关系
pip show flask

你会看到,只装了一个flask,但pip list里可能列了七八个包。这就是依赖传递的直观体现。

我个人习惯用pipdeptree这个工具来可视化依赖树:

pip install pipdeptree
pipdeptree

输出会像这样:

Flask==2.3.2
  - click [required: >=8.0, installed: 8.1.3]
  - itsdangerous [required: >=2.1.2, installed: 2.1.2]
  - Jinja2 [required: >=3.1.2, installed: 3.1.2]
    - MarkupSafe [required: >=2.0, installed: 2.1.3]
  - Werkzeug [required: >=2.3.3, installed: 2.3.6]

看到没?Flask依赖Jinja2,Jinja2又依赖MarkupSafe。这就是依赖树的层级结构。

💡 实用建议:当你遇到“装不上”或者“跑不起来”的问题时,第一件事就是跑pipdeptree看看依赖树。很多时候,问题就藏在那些你根本没听说过的间接依赖里。

1.5 本章小结

好了,这一章我们聊了三个核心概念:

  • 依赖:你的代码离不开的其他代码,分直接依赖和间接依赖
  • 依赖冲突:版本打架,原因包括版本范围不兼容、API变更、循环依赖等
  • pip:Python的包管理工具,负责安装、卸载、管理依赖

下一章,我们会深入聊聊requirements.txt的正确写法,以及如何用虚拟环境隔离依赖。这些都是实战中每天都要用的技能。

嗯,先消化这些吧。有问题随时翻回来看看。