啊哈!Python 环境管理原来如此简单

啊哈!Python 环境管理原来如此简单

在日常开发中,我经常使用 Python 内置的 venv 模块创建虚拟环境来满足不同的包(package)依赖要求,一切都显得岁月静好。直到我遇到了两个开源项目,它们对于 Python 环境的要求各不相同。项目A推荐使用 Python 版本3.8.1,理由是只有这个版本是通过全面测试的。为了满足这个要求,我选择了 miniconda 来管理多个 Python 版本,并成功地将项目A跑起来了。然而,当我需要使用项目B时,事情开始变得复杂起来。项目B的 install.sh 安装脚本默认安装了 pyenv 来管理 Python 环境。

就在我安装了项目B之后,我似乎打开了潘多拉魔盒,各种奇怪的错误开始出现:某个模块找不到、某个文件不存在。最初的解决策略是提示哪个找不到安装哪个,但到最后出现了更诡异的一幕:尽管在 pip 安装时已经提示安装成功,但在 Python 中 import 的时候找不到。最终,我发现问题的根源在于 pyenv 和 miniconda 之间的环境混乱。

这次经历,让我不得不重新审视 Python 的环境管理方案。本文将从使用者的角度分析两种方案的工作原理,解释不同方案中如何隔离安装多个 Python 版本;多版本 Python 的调用过程;Python 包的安装位置和调用过程。通过这些分析,我们将能够更加明智地选择适合自己的环境管理方案,避免不必要的问题。

接下来的实验和分析过程将在 Ubuntu 18.04 系统上进行。

回归本质:探索 Python 环境管理的核心

要理解 Python 环境管理方案的运作原理,我们需要回归到最基本的概念——系统路径。

当在命令行中输入一条命令的时候,操作系统首先需要知道去哪里找到这个命令。这里的命令对应着一个可执行文件或者一个可执行脚本,当找到和命令匹配的可执行文件或脚本的时候,就可以执行了。
ls 为例,通过 which 命令可以显示它所对应的可执行文件位置:

~$ which ls
/usr/bin/ls

同样的,which 命令本身也是一个可执行文件,我们也可以查看它的位置:

~$ which which
/usr/bin/which

通过添加 -a 参数,可以查看所有与命令匹配的路径名

~$ which -a which
/usr/bin/which
/bin/which

有趣的是,在系统中存在两个名为 which 的可执行文件。在 Linux 系统中,存在一个 PATH 环境变量,它定义了一系列目录。当用户在命令行中输入一个命令时,系统会按照这些目录的顺序来搜索可执行文件。如果在这些目录中找到了对应的可执行文件,系统就会执行它,并且是先找到谁,就执行谁。所以不带 -a 参数的 which 命令会定位到第一个匹配的文件。
通过查看PATH的内容,我们可以看到 /usr/bin 就是在 /bin 目录前面。

~$ echo -e ${PATH//:/\\n}
/home/spadeboy/.local/bin
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
/usr/games
/usr/local/games
/snap/bin

那么这些内容和 Python 的虚拟环境有什么关系呢?因为 Python 也是一个可执行文件。无论是通过系统自带的包管理工具安装还是通过源代码编译安装,最终实现的目标就是以下的二者之一:1. 获得python可执行文件,并将其’拷贝’到上面 $PATH 目录列表中;2. 获得 python 可执行文件,将其所在目录添加到 $PATH 目录列表中。
当运行 python 命令的时候,系统就会从上到下遍历 $PATH 中列出的目录,查找 python 的第一个匹配项。如果有多个python版本,系统将使用它第一个找到的。换句话说,如果希望系统使用特定版本的 python,就应该将该版本的可执行文件所在目录放到 $PATH 目录的顶部。这一点就是理解所有这些 Python 环境管理工具背后原理的关键。

下图展示了整体结构,无论是系统中直接安装的 Python,还是通过 miniconda 或 pyenv 管理的 Python 环境,它们都是建立在系统路径 $PATH 的基础之上。在这些环境之上,可以继续创建 Python 虚拟环境,让所有的 Python 应用都可以运行在虚拟环境之中,也可以直接运行在 miniconda、pyenv 和系统环境之上。

python envrionment structure

接下来我将说明各个部分的基本工作原理。

基本原理

Python 虚拟环境

Python 虚拟环境(virtual environments) 广泛用于依赖管理、隔离环境、无需系统管理员权限即可安装和使用 Python 包,以及跨多个 Python 版本进行自动化测试等用途。
自 Python 3.3 版本之后,Python 提供了一个内置的 venv 模块用于创建和管理虚拟环境1。在这之前,Python 主要依赖于第三方工具如 “virtualenv” 来管理虚拟环境。

创建虚拟环境

当使用 python -m venv <venv_name> 命令创建虚拟环境的时候,Python 解释器会被复制到新创建的虚拟环境目录中。
venv_name 目录下会创建一个 pyvenv.cfg 文件,其中包括几个 key/value 对。
这个 pyvenv.cfg 中的 home 键表明当前的 Python 解释器处于虚拟环境中,而 home 键的值是用于创建这个虚拟环境的 Python 可执行文件所在的目录。

激活虚拟环境

当通过执行 source bin/activate 来激活虚拟环境时,activate 脚本会将当前的 $PATH 值保存到 $_OLD_VIRTUAL_PATH 中,同时将虚拟环境的根目录添加到 $PATH 变量的首位。这样,再次运行 python 命令时,就会执行虚拟环境中的 Python 解释器。在执行 Python 二进制文件时,pyvenv.cfg 文件会成为查找的第一个步骤。而这个 pyvenv.cfg 文件所在的目录就会被设置为 sys.prefix 的值,Python 根据这个值来查找标准库和其他一些关键文件。
当执行 deactivate 命令时,退出 Python 虚拟环境,$PATH 值会恢复为 $_OLD_VIRTUAL_PATH 的值。

conda/miniconda

Conda 是一个开源的包管理系统和环境管理系统,可用于安装和管理软件包及其依赖项。它允许用户轻松地创建、导出、安装、移植和扩展软件环境。Conda 可以用于多种编程语言和领域,但最常用于 Python。Miniconda2 是 Conda 的精简版,仅包括 Conda 包管理器和 Python。这里以最常使用的 miniconda 为例进行介绍。

miniconda 安装过程

miniconda 安装过程很简单,它提供了一个安装脚本,只需下载并执行即可完成安装。安装完成后,需要进行一次性的初始化过程,即执行 ~/miniconda3/bin/conda init bash 命令。这个命令会修改 ~/.bashrc 文件,并添加一段代码,如下所示:

# >>> conda initialize >>>
# !! Contents within this block are managed by 'conda init' !!
__conda_setup="$('/home/spadeboy/miniconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)"
if [ $? -eq 0 ]; then
    eval "$__conda_setup"
else
    if [ -f "/home/spadeboy/miniconda3/etc/profile.d/conda.sh" ]; then
        . "/home/spadeboy/miniconda3/etc/profile.d/conda.sh"
    else
        export PATH="/home/spadeboy/miniconda3/bin:$PATH"
    fi
fi
unset __conda_setup
# <<< conda initialize <<<

这样,在每个新打开的终端中,都会自动执行这段代码,以修改环境变量。

PATH 变化
初始化后,查看 $PATH 的内容会发现 miniconda 所在的目录已经被添加到了 $PATH 的最顶部。

(base) spadeboy@conda:~$ echo -e ${PATH//:/\\n}
/home/spadeboy/miniconda3/bin
/home/spadeboy/miniconda3/condabin
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
/usr/games
/usr/local/games
/snap/bin

同时,查看环境中的 Python 可以发现,系统中内置的 Python 已经被 miniconda 中的 Python 取代,并且 Python 和 Python3 都被软链接到 python3.6:

(base) spadeboy@conda:~$ which python3
/home/spadeboy/miniconda3/bin/python3
(base) spadeboy@conda:~$ ls -lr /home/spadeboy/miniconda3/bin/python
lrwxrwxrwx 1 spadeboy spadeboy 10 May  8 11:20 /home/spadeboy/miniconda3/bin/python -> python3.6
(base) spadeboy@conda:~$ ls -lr /home/spadeboy/miniconda3/bin/python3
lrwxrwxrwx 1 spadeboy spadeboy 10 May  8 11:20 /home/spadeboy/miniconda3/bin/python3 -> python3.6
Conda 环境创建和管理

安装完 Miniconda 后,可以使用 conda create 命令创建 conda 环境,并在不同的环境中安装指定版本的 Python 解释器。

尽管绝大部分 Python 版本可以通过 conda 直接安装,但在安装某个 Python 版本之前,最好通过 conda search python 查看一下当前 channel 支持的 Python 版本列表。

conda 之上的 Python 虚拟环境

在系统中已经安装了不同版本的 Python 后,可以通过 conda env list 命令查看已经创建的 conda 环境。根据不同的项目需求,可以选择激活不同的 Conda 环境,例如执行 conda activate python3_11_0 命令即可激活名称为 python3_11_0 的 Conda 环境。

在 conda 环境中,可以直接使用 pip install 命令安装 package 到当前的 site-packages 位置
(python3_11_0) spadeboy@conda:~$ ls -lr /home/spadeboy/miniconda3/envs/python3_11_0/lib/python3.11/site-packages/

这里还有一种场景:两个不同的项目要求相同的 Python 版本,但其所依赖的包存在不同版本的要求。如果这个时候单独为两个项目创建两个不同的 conda 环境是可以解决问题的,但还存在另一个种解决方案:就是在 conda 环境中使用 Python 虚拟环境,利用 Python 自带的 venv 模块就能够很好的解决这个问题。

激活 conda 环境后,直接 python -m venv <venv_name> 创建一个虚拟环境目录,再次激活这个虚拟环境。

(python3_11_0) spadeboy@conda:~$ python -m venv .venv
(python3_11_0) spadeboy@conda:~$ source .venv/bin/activate
(.venv) (python3_11_0) spadeboy@conda:~$ 

这样在 conda 之上,又叠加了一层 Python 虚拟环境。此时,使用 pip 工具安装 package 就好了。强调一下,这个环境叠加的方式能够正常工作,同样是因为利用 venv 创建虚拟环境的时候,将虚拟环境目录添加到了 $PATH 的顶部。

pyenv

pyenv3 使用 shims 来管理不同版本的 Python 解释器。Shims 的中文含义是"垫片",这个翻译还是挺形象的,它是一种轻量级的代理脚本,位于 ~/.pyenv/shims 目录下。
当使用 pyenv 安装多个 Python 版本时,每个版本的 Python 解释器都有一个对应的 shim 文件。这些 shim 文件所在目录将出现在 PATH 环境变量中的最顶端,确保在执行 python 或其他 Python 相关命令时,优先调用 pyenv 的 shim,由它去调用版本匹配的 Python 文件,而不是系统中的默认 Python。

pyenv 安装 Python 过程

安装指定版本的 Python 时,pyenv 使用源码安装的方式,所以使用 pyenv 的时候需要有编译工具的支持。

~$ pyenv install 3.8.12
Downloading Python-3.8.12.tar.xz...
-> https://www.python.org/ftp/python/3.8.12/Python-3.8.12.tar.xz
Installing Python-3.8.12...

在利用 pyenv 安装 python 的时候,会将 python 安装到 ~/.pyenv/versions 目录下;将 shims 目录放置到 $PATH 路径列表的顶端。

Python 版本查询顺序

在确定使用的 Python 版本时,pyenv 会按照一定的顺序进行搜索,如下图所示:

pyenv python search sequence
如图所示,左侧是 pyenv 搜索匹配 Python 版本的顺序:从下至上优先级降低,首先搜索下方内容,如果下方内容找不到,就寻找上一个内容。 - `$PYENV_VERSION` 环境变量决定了当前 shell 会话中使用的 Python 版本。如果这个环境变量没有设置,pyenv 会查看当前路径中的 `.python-version` 文件,该文件记录了当前目录所绑定的 Python 版本。如果当前目录中没有 `.python-version` 文件,则会级联查找父级目录,仍是寻找 `.python-version` 文件,直到根目录为止。最后检查 `~/.pyenv/version` 文件是否存在,其中的内容记录为全局 Python 版本。

以上的4级搜索中对应着 pyenv shell, pyenv local, pyenv global 命令,这些命令可以用来设置以及查看各级 Python 环境版本。

pyenv 的一个重要特点就是 Python 环境可以绑定到路径上。例如,在某个目录下执行 pyenv local 3.9.7 命令会设置本地 Python 版本,只要进入这个目录或者它的子目录,执行过程中使用的 Python 版本都是 3.9.7。这其中的原理就是 pyenv local 3.9.7 命令会在当前文件夹下创建一个 .python-version 文件,文件的内容就是 “3.9.7”。这样的配置让开发者无需每次都手动切换环境,在一次配置之后实现了 Python 环境"开箱即用"的效果。

pyenv 之上的 Python 虚拟环境

在 pyenv 选定的 Python 环境下,可以直接使用 pip 安装需要的 package:

pplive@docker:~/workspace/pyenv_test$ pip install lxml 
Collecting lxml
  Downloading lxml-5.2.1-cp39-cp39-manylinux_2_28_x86_64.whl (5.0 MB)
     |????????????????????????????????| 5.0 MB 12.4 MB/s 
Installing collected packages: lxml
Successfully installed lxml-5.2.1

安装的 package 被放置到对应版本 Python 的 site-packages 目录下:

pplive@docker:~/workspace/pyenv_test$ pip show lxml
Name: lxml
Version: 5.2.1
Summary: Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API.
Home-page: https://lxml.de/
Author: lxml dev team
Author-email: lxml-dev@lxml.de
License: BSD-3-Clause
Location: /home/pplive/.pyenv/versions/3.9.7/lib/python3.9/site-packages
Requires: 
Required-by: 

在 pyenv 下仍然可能存在"两个不同的项目要求相同的 Python 版本,但其所依赖的包有不同版本的要求"的场景,这时候又可以召唤 python 自带的 venv 模块了。
执行 python 的 venv 模块,创建虚拟环境:

pplive@docker:~/workspace/pyenv_test$ python -m venv .venv 
pplive@docker:~/workspace/pyenv_test$ ls -lra
total 16
drwxrwxr-x 5 pplive pplive 4096 May  9 01:56 .venv
-rw-rw-r-- 1 pplive pplive    6 May  6 12:14 .python-version
drwxrwxr-x 8 pplive pplive 4096 May  7 02:41 ..
drwxrwxr-x 4 pplive pplive 4096 May  9 01:56 .

激活 Python 虚拟环境后,在其中安装 lxml,并查看其位置:

pplive@docker:~/workspace/pyenv_test$ source .venv/bin/activate
(.venv) pplive@docker:~/workspace/pyenv_test$ pip install lxml 
Collecting lxml
  Using cached lxml-5.2.1-cp39-cp39-manylinux_2_28_x86_64.whl (5.0 MB)
Installing collected packages: lxml
Successfully installed lxml-5.2.1
(.venv) pplive@docker:~/workspace/pyenv_test$ pip show lxml 
Name: lxml
Version: 5.2.1
Summary: Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API.
Home-page: https://lxml.de/
Author: lxml dev team
Author-email: lxml-dev@lxml.de
License: BSD-3-Clause
Location: /home/pplive/workspace/pyenv_test/.venv/lib/python3.9/site-packages
Requires: 
Required-by: 

小结

Python 拥有多个版本和丰富的 package 支持,在为开发者提供便利的同时也带来了版本适配、包依赖等问题。当在同一台机器上运行不同的 Python 项目时,可能面临 “不同 Python 版本” 和 “相同 Python 版本下不同 package 版本” 这两种情况,共涉及四种组合。
通过前文的分析,conda 和 pyenv 结合 python 内置的虚拟环境管理模块 venv 可以解决所有的问题。

在选择哪一个方案的时候,需要考虑的不是功能性问题,更多是以下一些细节:

  1. 安装方便性
    miniconda 和 pyenv 都提供一键安装脚本。如果没有科学上网问题,二者安装都很方便。
  2. Python 和包的安装便利性
    miniconda 存在多个 Channel 用于获得 Python 版本,如果某个特定版本的 Python 在一个 Channel 中不存在,需要尝试其他的 Channel 获取;pyenv 实际上是通过下载和安装 Python 的发布包(tarball)来安装各个 Python 版本的,所有版本一次性覆盖。
  3. 占用硬盘空间
    miniconda 相对于 Conda 已经做了很大的精简,但相对于 pyenv 这个完全脚本实现的工具来说,minicoda 仍然占用比较大的磁盘空间。
  4. 使用简易程度
    conda/minicoda 提供了非常丰富的feature,完全掌握门槛比较高,只是管理 python 环境有点儿牛刀杀鸡的感觉;pyenv 相对清爽很多。

重点
如果系统中已经使用了 miniconda,再引入 pyenv 可能就会出现灾难性事件了,比如我遇到的 miniconda 和 pyenv 环境混乱的问题。一个原则是系统中仅保留一套 python 环境管理工具。如果环境中已经存在了一套管理工具,为了避免冲突,可以考虑使用 Docker 容器来隔离不同应用所需的不同环境,这样可以更好地管理和维护环境。

参考

  1. Python Virtual Environments
  1. Miniconda
  1. pyenv
  • 19
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值