Python 虚拟环境详解

2015-11-30 language python

Python 的虚拟环境用来创建一个相对独立的执行环境,尤其是一些依赖的三方包,最常见的如不同项目依赖同一个但是不同版本的三方包,而且,在虚拟环境中的安装包不会影响到系统的安装包。

不过,其具体的工作原理是怎样的,这里详细介绍。

简介

在开发 Python 程序时,经常会遇到要使用不同版本的问题,例如 Python 的 3.4、2.7 甚至是 2.6 版本,甚至是多个应用场景,例如机器学习、Web 框架等等,而通过 pip 安装时,一般只会安装到标准路径下,这会导致包特别混乱,而 Python 构建独立环境又特别方便,所以,这里介绍下如何使用独立的虚拟环境。

这里所谓的虚拟环境,保证了其 Python 可执行文件的版本独立,而且安装的包也在该环境对应目录下,所以也是相对独立的。

环境准备

在 Python 2 中,通常使用 pyenv 用于在多个不同系统 Python 版本之间切换,而使用 virtualenv 用于创建独立的开发环境。其中 pyenv 是一堆的 Bash 脚本,安装方式可以参考 github pyenv 中的介绍,而在 Linux 中,也可以通过上述的 alternatives 工具进行切换,所以,这里就不详细介绍了。

virtualenv

对于 Python 2 来说,可以通过 virtualenv 来为一个应用创建一套 “隔离” 的 Python 运行环境,详细可以参考 Guide to Python 中的详细介绍。

直接通过 pip install virtualenv 安装相应的最新版本,假设要开发一个新的项目,需要一套独立的 Python 运行环境,那么就可以按照如下步骤操作:

----- 创建目录
$ mkdir /tmp/project && cd /tmp/project

----- 创建一个独立的Python运行环境,命名为foobar
$ virtualenv --no-site-packages foobar
New python executable in /tmp/project/foobar/bin/python2
Also creating executable in /tmp/project/foobar/bin/python
Installing setuptools, pip, wheel...done.

通过命令 virtualenv 可以创建一个独立的 Python 运行环境,参数 --no-site-packages 表示已安装到系统环境中的所有第三方包都不会复制过来,这样,我们就得到了一个不带任何第三方包的 “干净” 的 Python 运行环境。

激活环境

新建的 Python 环境被放到当前目录下的 foobar 目录,此时可以通过 source foobar/bin/activate 切换到这个 Python 环境;当然,不同的终端如 csh、fish 需要执行不同的脚本。

此时的终端提示符会添加 (foobar) 前缀,然后可以通过 pip install jinja2 类似的命令安装三方包,需要退出时执行 deactivate 命令。注意,默认会使用 Python2 的版本,可以通过 --python=python3.6 参数指定版本号。

基本原理

所谓的独立环境,无非就是解决两个问题:A) 执行 Python 解析器所使用的版本;B) 使用独立的包。其中前者,在 Linux 主要是通过 PATH 环境变量设置,在 activate 脚本中有如下的内容。

VIRTUAL_ENV="/tmp/project/foobar"
export VIRTUAL_ENV

_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH

也就是将创建的目录添加到 PATH 环境变量最开始,那么就会优先查找该路径,这样就解决了 python 解析器独立的问题,另外就是下面的包查找的顺序了。

venv

在 Python 3.3 以上的版本,在基础安装包中已经集成了基础的虚拟环境创建工具 venv,这样可以用标准方式代替之前的 virtualenv 工具。

----- 在子目录下创建名称为machine的虚拟环境
$ python -m venv machine

有些参数可以通过 python -m venv -h 查看详细的帮助信息,接着是要激活创建的环境。

----- 对于Linux、Unix、Mac等操作系统
$ source machine/bin/activate

----- 如果是在Windows CMD中
D:> machine/Scripts/activate.bat

----- 如果在Windows PowerShell下
PS D:> machine/Scripts/Activate.ps1

在激活虚拟环境后,命令行会提示当前虚拟环境的名称。

配置文件

如上,如果是 Python3 就可以直接使用内置的 venv 模块,其原理与上述的相同,同时通过 pyvenv.cfg 配置文件来标识原始的 home 位置,该文件的内容如下。

home = /usr/bin
include-system-site-packages = false
version = 3.6.8

如果 include-system-site-packagestrue,解释器启动时就会将系统的库添加到 sys.path 里面,这样在虚拟环境就可以 import 系统中安装的包了。

注意,Python3 提供的 venv 模块只能根据当前版本创建,不能支持 Python2 。

包查找顺序

如果要使用独立的包,那么关键就是如何在通过 import 导入时查找到所需的包。

包的查找顺序可以查看 Python 模块简介 中的介绍,简单来说,就是先查看是否是内置模块,然后再从 sys.path 列表指定的地址中搜索。所以,这里的关键就是 sys.path 列表的生成。

关于 sys.prefix

在 Python 启动时,会先加载一个强依赖的 os.py 包,而查找这个包是根据解析器的当前路径,以及固定的查找规则来实现的。

简单来说,就是在当前路径加上 lib/python${VERSION}/os.py 逐层向上查找,注意,如果是 64 位的操作系统,那么会使用 lib64 替换掉之前的 lib 路径。

例如,默认的 Python3 的解析器路径为 /usr/bin/python3.6 ,那么基础路径是 /usr/bin/ ,所以,其查找顺序为。

/usr/bin/lib64/python3.6/os.py
/usr/lib64/python3.6/os.py
/lib64/python3.6/os.py

只要在任意路径上找到 os.py 包,那么就会退出查找,并设置好 sys.prefix 变量,详细可以通过 strace python 查看,会有如下的搜索路径。

stat("/usr/bin/Modules/Setup", 0x7fffb7146300) = -1 ENOENT (No such file or directory)
stat("/usr/bin/lib64/python2.7/os.py", 0x7fffb71462f0) = -1 ENOENT (No such file or directory)
stat("/usr/bin/lib64/python2.7/os.pyc", 0x7fffb71462f0) = -1 ENOENT (No such file or directory)
stat("/usr/lib64/python2.7/os.py", {st_mode=S_IFREG|0644, st_size=25910, ...}) = 0

在查找到 os.py 之后,会将该路径设置为 sys.prefix 变量,然后解析器就会到 ${sys.prefix}/lib/python${VERSION} 目录下查找包。

总结

那么其工作原理就是,将 python 解析器保存在 ${VENV_PATH}/bin/python ,然后创建 ${VENV_PATH}/lib/python${VERSION} 目录,并将相关的文件复制到该目录下,可以复制文件,也可以使用软连接。

参考