Python项目中的包管理
1 前言
最近项目上用到了python,作为之前一直将python拿来写点小脚本的小白,都是直接在一个文件中写好需要实现的功能,本地直接用pip安装需要用到的依赖包, 然后能run起来就可以了。但是在工程实践中如何像Java项目那样来管理python项目的包呢,为了搞清楚各种包管理工具pip,uv,poetry,conda之间的关系, 今天就跟我一起来把它们彻底搞清楚。
2 从demo开始
为了弄清楚我们如何正确规范的管理一个python项目,以及常见的一些python项目中的包管理原理,我们从一个实际的例子开始:

这个python工程中只包含了一个main文件,仅仅引入了一个flask依赖,写了一个hello world。此时由于没有flask依赖,所以项目运行会报错。你的第一反应 是不是也是直接执行如下命令pip3 install flask安装一下就完美解决了?确实,这样项目可以成功运行,但是存在一个问题,如下终端显示:
PS E:\workspace\py-demo1> pip3 show flask
Name: flask
Version: 3.1.2
Summary: A simple framework for building complex web applications.
Home-page: None
Author: None
Author-email: None
License: None
Location: d:\program files\python\lib\site-packages
Requires: werkzeug, click, importlib-metadata, blinker, markupsafe, jinja2, itsdangerous
Required-by:
PS E:\workspace\py-demo1>
我们刚才安装的Flask包被安装到了python的全局路径下(标黄的部分),此时电脑上的所有项目都共享该Flask依赖,例如我这里是3.1.2,那么就代表所有项目 用到的Flask都是这个版本。
这样就会带来两个问题:
- 版本冲突
- 复杂的依赖关系

如图所示,当我们有两个项目都在依赖Flask且版本不一致时,此时发生依赖冲突;而一个依赖可能又会有它自己的其他依赖包,导致更多的冲突,完全无法管理。 那么如何解决这个问题呢,那就是虚拟环境。
3 虚拟环境
虚拟环境就是为了解决上面出现的问题而出现的,它的作用就是为每一个项目创建一个独立且干净的依赖环境,实现项目之间的依赖隔离。
虚拟环境的创建有两种方式:
- 命令创建:
python3 -m venv .venv - IDEA创建项目的时候自动创建。

两种创建方式效果一样,创建完成后我们的项目根目录下就多了一个.venv文件夹。这里的.venv就是虚拟环境的名字,虽然也可以取其它,但是不建议这样做。
现在的很多IDE都能识别这个.venv名字的目录作为我们的虚拟环境,如果我们在命令行执行,则需要执行如下代码让环境生效:
# linux激活虚拟环境
source .ven/bin/activate
# windows激活虚拟环境(正常IDEA可以识别,无需手动激活)
./venv/Scripts/activate
此时我们再执行pip3 install flask命令安装依赖,就会安装到我们的虚拟环境目录下了。

到这里我们就解决了每个项目维护自己专属的依赖库,而没有了冲突的烦恼。
虚拟环境底层原理:
主要是修改了python中
sys.path这个变量,这个变量的值是一个列表,里面记录了python再导入模块时需要搜索的文件夹路径。当我们import依赖时, python就会逐个搜索这些路径,直到找到import的包。虚拟环境被激活后,我们得到虚拟目录会被添加到
sys.path这个搜索列表。这样就可以搜索到 虚拟目录下的依赖包了。当没有虚拟目录时,
sys.path包含的是我们python的全局目录。
这就完了吗?显然没有!试想一下,Java项目中我们使用maven来管理依赖,别人看到我们的项目时,可以清晰的看到pom中依赖的版本和名称,那么python中如何
把我们的项目依赖这样清晰的展示给别人呢?总不能一行行的执行pip install命令吧(虽然我这样干过)。
早期的做法是使用pip freeze命令,它可以打印出当前虚拟环境中所有已经安装好的包和他们的版本号:
(.venv) PS E:\workspace\py-demo1> pip freeze
blinker==1.9.0
click==8.1.8
colorama==0.4.6
flask==3.1.2
importlib-metadata==8.7.0
itsdangerous==2.2.0
jinja2==3.1.6
markupsafe==3.0.3
werkzeug==3.1.4
zipp==3.23.0
然后把这些包信息重定向到一个名叫requirements.txt的文件中,然后别人就可以看到你依赖的软件包,当别人需要在自己的环境运行你的项目时,
执行pip install -r requirements.txt就可以轻松安装依赖了。
现在很多开源项目中也仍然使用的是这样的方式,包括我司现在也是这样的方式在管理项目依赖。但是这样有一个很大的缺陷:
pip freeze无法分清哪些是我们项目的直接依赖,哪些是这些依赖引入的间接依赖。
如上面的执行结果,我的项目中仅仅依赖到了一个Flask包,但是导出的包却有很多间接依赖。当项目比较复杂的时候,管理起来就会失控,你会陷入各种各样的安装 依赖错误的问题中。(我曾深陷其中无法自拔)
另外一个问题就是,当我们执行卸载操作的时候,pip只会移除你指定的包本身,那些间接依赖是不会被删除的,这就会在我们的项目中成为没人使用的孤儿。

我直接说解决办法,那就是pyproject。
4 pyproject
现代python项目通常是使用pyproject.toml来解决pip freeze带来的孤儿依赖和间接依赖问题。这个文件是官方指定的统一配置文件。在它成为标准之前,
不同的开发工具通常有各自独立的配置文件,如下图:

当项目中使用的工具增多时,根目录下就会出现大量零散的配置文件。现在python生态中大多数工具都支持了pyproject.toml,所以我们就只需要维护这一个配
置文件就可以了。
我们只需要在pyproject.toml配置文件中添加一个dependencies配置,就可以替代requirements.txt了,如下所示:
[project]
name = "demo-python"
version = "1.0.0"
dependencies = [
"Flask == 3.1.2"
]
我们只需要在配置中声明项目的直接依赖Flask即可,而不用关心那些间接依赖了。如果我们想要删除该依赖,直接在配置文件中删除Flask这一行配置就好了,不会 留下任何孤儿依赖。
依赖写好后我们只需要执行:pip install .就可以安装依赖了。这里的.代表当前目录。
(.venv) PS E:\workspace\py-demo1> pip install .
Processing e:\workspace\py-demo1
Installing build dependencies ... done
Getting requirements to build wheel ... done
Preparing wheel metadata ... done
Building wheels for collected packages: main
Building wheel for main (PEP 517) ... done
Created wheel for main: filename=main-0.0.0-py3-none-any.whl size=1161 sha256=02b04b5edaf569687e0c570c8028a4af0ced71cc99beb0f4178bba3cf340fd27
Stored in directory: c:\users\luliangwei\appdata\local\pip\cache\wheels\42\bd\c4\e961192f846b57e5f0fbca35aa9fcb80d94c81ac2a5c21d492
Successfully built main
Installing collected packages: main
Successfully installed main-0.0.0
通过上述日志打印,可以看到实际执行了两个步骤:
- 打包当前目录为python标准软件包;(Created wheel for main)
- 安装该软件包,并把声明的依赖一并安装进来;(Installing collected packages: main)
这样会带来一个在开发阶段比较麻烦的新问题,那就是我们的源代码文件也会被安装到虚拟环境中的目录中。如下图所示,我的源代码main.py文件被安装到了
虚拟环境的site-packages目录中。

这样我们的项目中就存在两份相同的main.py文件,如果我们修改了源代码,这个代码并不会自动同步到虚拟环境中。所以我们需要在安装依赖时带上一个参数,
这样就不会把源代码复制到site-packages目录中了。
# 解决源码文件安装到环境目录中的问题
pip install -e .
5 回顾一下
在前面的步骤中,我们用.venv环境冲突问题,用pyproject.toml解决了依赖管理问题。当你以为大功告成的时候,现实却给你当头一棒。因为此时你会发现,我们
安装软件包不能再像以前一样直接执行pip install xxx来安装了,每次安装包前,我们都需要去官网找到软件包的名称,然后找到对应的版本号,将它写到
pyproject.toml文件中,这样既容易出错,又不专业。此时你可能以为肯定官方提供了完成这个过程的方式,不用我们手动操作,但实际上是:没有!!!
虽然官方没有提供,但是社区有啊!!!
社区为我们提供了更高级的管理工具,如:poetry,uv,PDM。可以把它们理解为对venv和pip的高级封装,下面我们就继续一探究竟。
6 更高级的管理
6.1 uv
仍然以前面的用例来学习,我们将之前安装的所有东西都去掉,只在项目中保留main.py和一个项目配置文件pyproject.toml,并且该配置文件中,只包含了项目
名称和版本号。按照我们之前手动安装依赖的方式,有如下几步:
# 1. 创建虚拟环境
python -m venv .venv
# 2. 使虚拟环境生效
source .venv/bin/activate
# 3. 手动编辑配置文件,添加Flask依赖和版本号
edit project.toml
# 4. 安装依赖
pip install -e .
现在你可以忘了她(它)了。我们直接一条命令就可以完成上面的所有步骤:
uv add flask
当别人拿到我们的项目时,只需要执行uv sync,就可以自动创建虚拟环境并安装好相应的依赖了。此时就是如何启动项目的问题了,我们可以用如下两种方式来运行:
# 1. 使用传统方式
source .venv/bin/activate
python main.py
# 2. 直接使用uv启动(推荐)
uv run main.python
关于uv的安装方式,可以看看官方文档:https://uv.doczh.com/getting-started/installation/
我们可以直接在github release上下载对应的软件包进行安装,并配置环境变量。也可以使用官方提供 的安装脚本进行安装:
# windows安装
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
# linux安装
curl -LsSf https://astral.sh/uv/install.sh | sh
# 也可以指定版本安装
curl -LsSf https://astral.sh/uv/0.7.4/install.sh | sh
接下来我们来一个实战,看看如何在pycharm中创建一个python项目,并且使用uv进行依赖管理

pycharm会自动识别我们系统的uv,如果没有识别到,则自行指定uv的目录即可。创建好项目后,uv就会自动给我们创建好.venv目录
和pyproject.toml来管理依赖了。创建好的如下截图所示:
