聊聊 Python 中的依赖管理工具

聊聊 Python 中的依赖管理工具

相信初学 Python 的伙伴都碰到过这个麻烦事,想装个环境,在搜索引擎上一搜,蹦出来不少让人迷惑的字眼,比如 pip、virtualenv、virtualenvwrapper、venv、pyenv 等。某篇教程可能用的是这个工具,另一篇教程又可能是那个工具,还有的教程可能几个工具都在用。看了几篇后,整个人都懵了,这些工具各自都是干嘛的啊?就算最后环境弄好了,估计心里还在犯嘀咕,这些有什么区别和联系呢?好了,本篇文章就带你捋捋常见的一些包、环境管理工具,让你对这些工具有个相对清晰的认识。

先说下文章的思路,我会以碰到问题、解决问题的方式,引入几个典型工具的简单使用,从而理解它们的定位,然后根据定位梳理认识下现在比较流行的工具。这样一来,每个工具干什么的,该选用哪个工具,自己心里应该也清楚了。

Pyenv

现在的 Unix/Linux 系统基本上都装有 Python 解释器,不过都是 Python 2 版本的,这个版本已经在今年(2020 年)1 月 1 日宣布停止维护了,所以我们学习的话基本上都会选择 Python 3。这就带来一个问题,Python 3 怎么安装?

很多伙伴估计会使用系统上的包管理工具去安装,比如 apt、yum 等。如果想安装的某个版本在前面的工具中没法安装的时候,也会选择从官网下载源码,自己编译安装。这样做其实是有一定风险的,系统的不少软件是运行在 Python 2 上面,如果我们直接将系统层面的 Python 替换成 Python 3,可能会导致某些程序运行错误。有的伙伴可能会说,我将 Python 3 的可执行文件放到其它目录,需要使用的时候指定全路径使用,这当然是可以的。不过当后面又需要安装其它版本的 Python 的时候,这样操作起来是比较繁琐的。这个时候,我们就需要一款 Python 的版本管理工具,它能帮我们轻松的完成不同版本 Python 的安装以及切换,其实 Pyenv 就是来干这个事情的。

PS:文中部分工具我没有写安装过程,主要是将关注点集中在工具的功能上面,如果要安装可以参考官方文档

来看下 Pyenv 的简单使用,查看当前已安装的 Python 版本

$ pyenv versions
* system (set by /Users/kevinbai/.pyenv/version)

表示当前只有 1 个版本的 Python,前面的星号表示当前正在使用的 Python 版本。

查看当前可安装的版本

$ pyenv install --list
Available versions:
  2.1.3
  2.2.3
  2.3.7
  ...

这里面不仅列出了可安装的 CPython 版本,还会列出 Jython、PyPy 的版本(为使文章简短,上面的例子对这些进行了省略,没有列出来)。

安装 Python 3.7.5

$ pyenv install 3.7.5

这个命令会直接从官网下载某个版本的源码文件,并自动编译安装。怎么样?安装是不是特别简单。

怎么使用呢?你可以在某个目录下指定 Python 版本

$ pyenv local 3.7.5

这会在当前目录下创建一个 .python-version 文件,内容是 3.7.5,这样你在该目录及其子目录下启动 Python 解释器的时候,都是使用的该版本。可以看下版本

$ python --version
Python 3.7.5

如果你想在全局范围内指定版本,可以这样

$ pyenv global 3.7.5

这会创建文件 $(pyenv root)/version(如果不存在的话),并将其内容修改为 3.7.5,这样我们不管在哪个目录,启动的都是 Python 3.7.5 的解释器。

Pip

使用 Python 的好处之一就是,包很多。怎么安装包呢?很简单

$ pip install flask

这样 flask 就安装好了,然后 Python 代码中导入使用即可

>>> from flask import Flask
>>> app = Flask(__name__)

也即,Pip 的作用就是用来安装包的。

大部分情况下,当 Python 解释器安装好的同时,Pip 也安装好了。可以使用下面的命令检查 Pip 是否安装

$ pip --version
pip 19.2.3 from /Users/kevinbai/.pyenv/versions/3.7.5/lib/python3.7/site-packages/pip (python 3.7)

当出现类似的输入时,表明 Pip 已经安装成功了。如果没有,可以使用下面的命令进行安装

$ curl https://bootstrap.pypa.io/get-pip.py | python

Virtualenv

在本地开发完代码准备部署到线上的时候,你是怎么样记录所需的包然后安装到服务器的呢?

一般的做法是本地使用 Pip 生成依赖文件

$ pip freeze > requirements.txt

这会将当前安装的包以及相应版本记录到 requirements.txt 文件中,内容类似这样

click==7.1.2
Flask==1.1.2
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
Werkzeug==1.0.1

然后在服务器上执行

$ pip install -r requirements.txt

这样操作对于单个项目来说是 OK 的。但实际情况是,我们一般会开发维护多个项目,并且大部分项目的 Python 版本是一致的,如果按上面操作来的话,requirements.txt 中记录的是所有项目的依赖,这样在线上部署某个项目的时候,会安装不必要的包。另外,不同项目可能会使用不同版本的包,比如 A 项目需要 Flask 0.12.5,而 B 项目需要 1.0.3。这些问题怎么解决呢?

这里就要引入 Virtualenv 了,它的作用是为不同的项目创建不同的虚拟环境,每个项目安装的包都在不同的虚拟环境中。这样我们使用 Pip 导出 requirements.txt 的时候,导出的便是当前项目所需依赖。

来简单了解下 Virtualenv 的使用,安装 virtualenv

$ pip install virtualenv

安装好后,查看下当前 Python 解释器下已安装的包

$ pip list
Package            Version
------------------ -------
appdirs            1.4.4
click              7.1.2
distlib            0.3.0
filelock           3.0.12
Flask              1.1.2
importlib-metadata 1.6.1
itsdangerous       1.1.0
Jinja2             2.11.2
MarkupSafe         1.1.1
pip                19.2.3
setuptools         41.2.0
six                1.15.0
virtualenv         20.0.23
Werkzeug           1.0.1
zipp               3.1.0

进入项目目录 proj1 下面,创建虚拟环境 proj1_venv

$ virtualenv proj1_venv

激活当前虚拟环境

$ source proj1_venv/bin/activate

这时你会发现命令提示符前面多了个标识 (proj1_venv),说明当前已经在虚拟环境 proj1_venv 中了。看下解释器路径

$ which python
/Users/kevinbai/temp/proj1/proj1_venv/bin/python

当前的解释器路径已经变为虚拟环境下的解释器了。

看下已安装的包

(proj1_venv) $ pip list
Package    Version
---------- -------
pip        20.1.1
setuptools 47.1.1
wheel      0.34.2

因为是新创建的环境,所以并没有其它第三方包。安装个 Flask 试试

(proj1_venv) $ pip install flask
...
(proj1_venv) $ pip list
Package      Version
------------ -------
click        7.1.2
Flask        1.1.2
itsdangerous 1.1.0
Jinja2       2.11.2
MarkupSafe   1.1.1
pip          20.1.1
setuptools   47.1.1
Werkzeug     1.0.1
wheel        0.34.2

新安装的包会安装在当前虚拟环境中,所以之后使用 Pip 导出 requirements.txt 的时候,对应的依赖就是当前项目的。

如果需要退出虚拟环境,使用 deactivate 命令

(proj1_venv) $ deactivate

退出成功后,命令行前面的标识 (proj1_venv) 会消失。

pip-tools

上面管理依赖的方式,还是有需要完善的点。当我们安装 Flask 后导出的 requirements.txt 是这样的

click==7.1.2
Flask==1.1.2
itsdangerous==1.1.0
Jinja2==2.11.2
MarkupSafe==1.1.1
Werkzeug==1.0.1

其实这里我们需要的只是 Flask,但如果你对 Flask 不熟悉,过了一段时间后,你来看这个依赖文件,你完全不知道自己最初想装的那个包到底是啥。

另外,如果卸载 Flask,一般会执行命令

$ pip uninstall flask

这只会卸载 Flask,而 Flask 依赖的其它包比如 Werkzeug 等,都不会被删除,这导致环境中又有了其它不必要的包。当包比较少的时候,一个一个手动删除多余的包还是比较好操作,但是当包多后,不少包之间还存在依赖,还手动去处理,那就太麻烦了。

还有,有些包可能只在开发环境会使用到,比如 Pytest 等,所以将开发、生产等环境需要的依赖分开保存也是需要完善的一个点,单纯使用 Pip 的话没法将这个步骤自动化。

解决上面的问题可以使用 pip-tools,其主要包含两个工具:pip-compile 和 pip-sync。前者将原始的依赖转为比较详细的依赖文件(可以理解为上面例子中的 requirements.txt),后者使用前者生成的依赖文件安装、卸载或更新环境中的包,以保持依赖一致。如果要对依赖分类保存的话,只要先对原始的依赖进行分类保存即可。

简单了解下用法。还是先创建个虚拟环境,并安装 pip-tools

$ virtualenv proj2_venv
$ source proj2_venv/bin/activate
(proj2_venv) $ pip install pip-tools

创建 2 个原始依赖文件

# requirements_prod.in
flask
pymongo

# requirements_dev.in
-r requirements_prod.in
pytest

前者用于生产环境,后者用于开发环境。之后使用 pip-compile 生成详细依赖文件

(proj2_venv) $ pip-compile requirements_dev.in

这会在当前目录下生成 requirements_dev.txt 文件,内容类似

#
# This file is autogenerated by pip-compile
# To update, run:
#
#    pip-compile requirements_dev.in
#
attrs==19.3.0             # via pytest
click==7.1.2              # via flask
flask==1.1.2              # via -r requirements-prod.in
importlib-metadata==1.6.1  # via pluggy, pytest
itsdangerous==1.1.0       # via flask
jinja2==2.11.2            # via flask
markupsafe==1.1.1         # via jinja2
more-itertools==8.4.0     # via pytest
packaging==20.4           # via pytest
pluggy==0.13.1            # via pytest
py==1.8.1                 # via pytest
pymongo==3.10.1           # via -r requirements-prod.in
pyparsing==2.4.7          # via packaging
pytest==5.4.3             # via -r requirements-dev.in
six==1.15.0               # via packaging
wcwidth==0.2.4            # via pytest
werkzeug==1.0.1           # via flask
zipp==3.1.0               # via importlib-metadata

最后使用 pip-sync 将环境中的包与详细依赖文件中的包保持一致

(proj2_venv) $ pip-sync requirements_dev.txt

这个命令会参考 requirements_dev.txt,如果环境中有多余的包,这些包会被卸载;如果没有,则会被安装。当然,版本号不一致的话也会更新保持一致。

常规的使用方式是,有多少个原始依赖文件,我们都要生成对应的详细依赖文件,并将原始的和生成的加入版本控制,便于在不同的情况下使用。这里我们再生成下生产环境的详细依赖文件

(proj2_venv) $ pip-compile requirements_prod.in

如果开发过程中添加或删除包,只需要修改相应的原始依赖文件,然后重复上面的步骤即可。

Pipenv

上面的工具处理了依赖管理相关的不同问题,使用起来是 OK 的。不过还是有个不足的地方,就是重复性工作有点多。我们每次开启新项目,都要手动进行这些操作

  • 如果要使用不同版本的解释器,我们需要先进行安装
  • 创建新的虚拟环境
  • 手动修改依赖文件,然后再安装对应的包

于是就有了一些工具,尽量的将这这些流程自动化起来,Pipenv 就是其中一个,我们还是举一些例子。

我们创建项目 proj3,并进入到该目录下

$ pipenv --python 3.7.5

这会执行 3 个主要操作

  • 如果当前环境没有安装 Python 3.7.5 的话,Pipenv 会使用 Pyenv 进行安装;如果没有 Pyenv 的话,会提示你进行安装
  • 在 $HOME/.local/share/virtualenvs 目录下创建虚拟环境,目录名类似 proj3-wulHnaWi
  • 在项目目录下生成 Pipfile 用于管理依赖

Pipfile 内容如下

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]

[requires]
python_version = "3.7"

相当于这里面也区分了生产环境和开发环境的依赖。安装包的话也很方便

$ pipenv install flask

安装成功后,原始依赖信息会记录到 Pipfile 中

[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
flask = "*"

[requires]
python_version = "3.7"

同时会生成一个 Pipfile.lock 文件,该文件记录了安装包的具体版本。

也就是说,当我们安装或者卸载包的时候,变动会自动记录到 Pipfile 以及 Pipfile.lock 文件中,就不用我们手动去维护了。

一般我们会把这 2 个文件加入版本控制。如果你要在生产环境部署,只需要执行

$ pipenv install

会自动给你生成相应的虚拟环境并安装 Pipfile.lock 中的包,线上环境和开发环境的包的版本可以达到完全一致。

如果你要在当前 shell 激活虚拟环境,执行

$ pipenv shell

和 Virtualenv 类似,这也会在命令提示符前添加一个标识 proj3。如果要退出的话,执行

(proj3) $ exit

退出成功的话,命令提示符前的标识 proj3 也会消失。

Conda

Conda 也是一款包和环境管理工具,它可以干以下事情

  • 支持不同版本 Python 的安装
  • 包安装
  • 虚拟环境管理

但它并不局限于 Python,这是官网对它的描述

Package, dependency and environment management for any language---Python, R, Ruby, Lua, Scala, Java, JavaScript, C/ C++, FORTRAN

我的理解是,Conda 将不同语言的解释器、常用的包以及其底层依赖其它语言的库都已经提前编译好放在 Conda 的包管理列表上面,Conda 安装包的时候就直接从那上面去下载下来放到特定位置就行,这就是 Conda 的包管理。至于环境管理,和 virtualenv 类似,创建不同的虚拟环境目录,然后根据需要在不同环境中安装不同的包。

单就 Python 而言,依赖管理已经有 Pip 这样的工具了,为什么 Conda 还别树一帜,搞自己的包管理呢?Conda 的使用者更多的集中在数据科学领域,这些领域涉及大量的计算,纯 Python 计算效率较低,很多包都是使用 C/C++ 实现的。直接使用 Pip 安装的话会在本地进行编译,由于本地缺少某些动态库等各种原因,编译时出错的可能性比较大,这对一些编程经验不多的人来说,解决起来还是有点麻烦。Conda 出于这方面考虑,提前将依赖编译好,降低了安装依赖的难度,从而让使用者将重点放在算法、业务逻辑上面。

说起 Conda,不得不提起 Anaconda 和 Miniconda,它们之间有什么关系呢?Anaconda 和 Miniconda 是 Conda 的不同发行版,都包含有 Conda 工具。Miniconda 只包含 Pyhton、Conda、zlib 等少量常用的包以及它们的一些依赖,目标是尽可能的精简;Anaconda 呢,正好相反,将另外的数据科学领域常用的 160 多个包都放了进来。如果你喜欢按需安装使用相关的包,追求精简,可以选择 Miniconda,不然就选择 Anaconda。

不管选择的是 Miniconda 还是 Anaconda,安装的话直接去官网选择对应系统的安装包安装就行。下面来体验下 Conda 的用法吧。

查看所有的虚拟环境

$ conda info --envs
base                  *  /Users/kevinbai/miniconda3

说明目前只有 base 环境,其中星号表示当前所在环境。查看当前环境已安装的包

$ conda list
# packages in environment at /Users/kevinbai/miniconda3:
#
# Name                    Version                   Build  Channel
...
python                    3.7.7                hf48f09d_4
...

目前的 Python 版本为 3.7.7,如果我们需要创建一个 Python 3.8 的环境 py38,可以这样

$ conda create --name py38 python=3.8

如果环境名之后不跟上名字,则会创建一个什么包都没装的环境;如果需要创建时安装其它包,则和 python=3.8 一样将包名写在环境名后面就行,不指定版本的话会安装最新的。

切换到 py38

$ conda activate py38

激活某个环境后,命令提示符前仍然会添加标志符 (py38)

如果要安装包,比如 beautifulsoup4

(py38) $ conda install beautifulsoup4

导出依赖文件

(py38) $ conda env export > environment.yml

文件内容类似这样

name: py38
channels:
  - defaults
dependencies:
  - beautifulsoup4=4.9.1=py38_0
  - ...
prefix: /Users/kevinbai/miniconda3/envs/py38

根据依赖文件更新虚拟环境

(py38) $ conda env update -f environment.yml

如果要退出虚拟环境,执行以下命令

(py38) $ conda deactivate

如果有些包只在 PyPi 才有怎么办呢?这就需要结合 Pip 来使用了,比较好的实践教程可以参考 Anaconda 官方的博客 Using Pip in a Conda Environment。除此之外,Conda 和上面的工具比如 Pyenv、Pipenv 等都是可以结合使用的,不过这都需要你对相关工具比较熟悉才能比较好的结合起来。我用 Conda 比较少,如何才是比较好的实践,就需要大伙自己去摸索下了。

小结

通过前面的内容,你应该对依赖管理中常见的问题比较清楚了

  1. Python 版本管理
  2. 虚拟环境管理
  3. 依赖安装以及各个环境下依赖的统一
  4. 依赖管理工作流自动化

针对特定的问题,我们也了解一些对应的典型工具

  1. Pyenv
  2. Virtualenv
  3. Pip、pip-tools
  4. Pipenv、Conda

上面的工具与要解决的问题序号一一对应。

当然,除了上面的工具,还有很多也是比较常见的,我们简单过下

  • venv

这是 Python 标准库里的一个模块,是从 Virtualenv 中抽离了一些常用的功能组成的。功能虽说没有后者全,但是一般的环境管理还是够用了。

  • virtualenvwrapper

Virtualenv 的一个扩展,对虚拟环境的管理做了些增强,比如:将虚拟环境都放在一个目录下进行管理;提供了一些便携的操作,比如创建、删除、复制虚拟环境;不同的虚拟环境操作可以加一些钩子做一些自动化的事情等等。

  • pyenv-virtualenv

Pyenv 的一个插件,赋予了 Pyenv 虚拟环境管理的功能,底层其实也是依赖 Virtualenv 或者 venv 实现的。

  • Poetry

和 Pyenv 类似,关注依赖管理工作流的自动化,不过没有 Python 版本管理的功能,这一点结合 Pyenv 可以很好的解决。和 Pipenv 相比,有两个明显的特点:一是,Poetry 每次 install 都会将当前项目打包到环境里面;二是,个人觉得 Poetry 解析依赖更快一些,之前有段时间就是因为 Pipenv 解析太慢,也碰到一些 Bug,就放弃使用 Pipenv 了。

  • Pipx

这个名字容易让人觉得它也是用来管理依赖的,其实不然,它的定位是用来安装 Python 命令行程序的,比如上面的 Poetry 就可以通过它来安装。虽说 Pip 也能干这个,但 Pipx 做得更多(当然安装包的时候,Pipx 还是依赖 Pip)。它将安装的每个包都放在与包同名的虚拟环境(默认在 $HOME/.local/venvs 下)中,如果需要也可以指定 Python 版本,然后再将包的执行脚本通过软链接的方式添加到 $HOME/.local/bin 中,你可以将这个目录添加到 PATH,这样一来,你不用指定 Python 版本或者环境,就可以执行相应的程序。

另外,它可以在没有安装某个包的情况下,临时执行某个程序。简单说下这个过程,Pipx 会将要使用的包下载到本地并缓存,然后执行,过一段时间会将这些缓存删除,所以如果在短时间内再次运行同一个程序,不会重新下载,启动速度还是比较快的

到了这,相信大伙对 Python 依赖管理的相关工具已经比较了解了。但是,因为解决同一个问题,大都对应不止一个工具,怎么选择呢?这里我说下我的思路

  • 如果你想操作简单,不费事,可以选择基于工作流的。如果是数据科学领域,推荐 Conda,Anaconda 或者 Miniconda 都可以;其它领域比如后端、爬虫等,Pipenv 和 Poetry 都比较合适。你可以都试一试,找出你觉得顺手的一个
  • 如果你想每个步骤都手动来处理,也感觉这其实并不是很麻烦,那你可以自己组装工具。我例举了几个常见的搭配,同样,你可以都玩玩,选择顺手的那个
    • Pyenv、venv、pip-tools
    • Pyenv(这里 Pyenv 安装了 pyenv-virtualenv 这个插件用于管理虚拟环境)、pip-tools,这种方式我平时使用比较多
    • Pyenv(同样也安装了 pyenv-virtualenv 这个插件用于管理虚拟环境)、Pip,如果依赖比较少,或者不那么在意依赖的统一,这样其实也是可以的,这个我也用的比较多
  • 当然,如果团队中都在使用某个或者某套工具,跟着使用就行

好了,到这文章就要结束了,相信大家以后在看到相关工具时应该不会疑惑它们是干啥的了,也能根据自己的需要选择适合自己的工具。

参考

发表评论

电子邮件地址不会被公开。 必填项已用*标注