python虚拟环境,允许你在同一台机器上部署不同版本的python项目,即便是同一个python版本的项目,其依赖的第三方库也可以不同。在python2.7时代,实现虚拟环境最流行的库是virtualenv, 进入到python3时代后,内置了venv来支持虚拟环境的创建,此外,conda也可以非常方便的管理虚拟环境。
那么,你是否好奇过,虚拟环境是如何实现的,不同的环境之间究竟是如何做到隔离的,进入虚拟环境后,在shell命令行的最前端,还是显示虚拟环境的名称,这又是怎么实现的?
带着这些疑问,本文以virtualenv为例,向你解密python虚拟环境是如何创建的。
1. virtualenv
首先来安装virtualenv
pip install virtualenv
第二步,创建一个文件夹
mkdir test_env
cd test_env
第三步,创建虚拟环境
virtualenv cool
此时,在test_env目录下,会生成一个名为cool的目录,其目录结构为
cool/
├── bin
│ ├── activate
│ ├── activate.csh
│ ├── activate.fish
│ ├── activate.ps1
│ ├── activate_this.py
│ ├── activate.xsh
│ ├── easy_install
│ ├── easy_install3
│ ├── easy_install-3.7
│ ├── easy_install3.7
│ ├── pip
│ ├── pip3
│ ├── pip-3.7
│ ├── pip3.7
│ ├── python
│ ├── python3 -> python
│ ├── python3.7 -> python
│ ├── wheel
│ ├── wheel3
│ ├── wheel-3.7
│ └── wheel3.7
├── lib
│ └── python3.7
│ └── site-packages
└── pyvenv.cfg
从目录结构来看,所谓的虚拟环境,就是一个完整的python安装环境,lib/python3.7/site-packages用来安装第三方依赖。
想要使用这个虚拟环境,还需要正式进入其中,第四步,进入虚拟环境
[root@bffd06f7a14d test_env]# source cool/bin/activate
(cool) [root@bffd06f7a14d test_env]#
执行此命令后,正式进入虚拟环境,shell的命令行提示符也发生的变化,进入虚拟环境以后,使用pip安装的第三方模块,都会被安装在cool/lib/python3.7/site-packages中,使用python命令,启动的python解释器是cool/bin/python,这一点是如何做到的,只要能破解这个难题,也就理解了python虚拟环境。
2. activate
2.1 修改PATH环境变量
python虚拟环境的关键点是bin目录下的activate文件,打开这个文件,内容为
# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly
if [ "${BASH_SOURCE-}" = "$0" ]; then
echo "You must source this script: \$ source $0" >&2
exit 33
fi
deactivate () {
unset -f pydoc >/dev/null 2>&1 || true
# reset old environment variables
# ! [ -z ${VAR+_} ] returns true if VAR is declared at all
if ! [ -z "${_OLD_VIRTUAL_PATH:+_}" ] ; then
PATH="$_OLD_VIRTUAL_PATH"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if ! [ -z "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then
PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# The hash command must be called to get it to forget past
# commands. Without forgetting past commands the $PATH changes
# we made may not be respected
hash -r 2>/dev/null
if ! [ -z "${_OLD_VIRTUAL_PS1+_}" ] ; then
PS1="$_OLD_VIRTUAL_PS1"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
if [ ! "${1-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
VIRTUAL_ENV='/home/test_env/cool'
if ([ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]) && $(command -v cygpath &> /dev/null) ; then
VIRTUAL_ENV=$(cygpath -u "$VIRTUAL_ENV")
fi
export VIRTUAL_ENV
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH" # 修改PATH环境变量
export PATH
# unset PYTHONHOME if set
if ! [ -z "${PYTHONHOME+_}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1-}"
if [ "x" != x ] ; then
PS1="${PS1-}"
else
PS1="(`basename \"$VIRTUAL_ENV\"`) ${PS1-}"
fi
export PS1
fi
# Make sure to unalias pydoc if it's already there
alias pydoc 2>/dev/null >/dev/null && unalias pydoc || true
pydoc () {
python -m pydoc "$@"
}
# The hash command must be called to get it to forget past
# commands. Without forgetting past commands the $PATH changes
# we made may not be respected
hash -r 2>/dev/null
这段shell代码,最核心的部分是
VIRTUAL_ENV='/home/test_env/cool'
if ([ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]) && $(command -v cygpath &> /dev/null) ; then
VIRTUAL_ENV=$(cygpath -u "$VIRTUAL_ENV")
fi
export VIRTUAL_ENV
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH" # 修改PATH环境变量
export PATH
使用virtualenv创建虚拟环境时,生成了activate文件,VIRTUAL_ENV 的值是在文件生成时确定下来的,这段代码的目的是修改PATH环境变量。我们都知道,当我们在命令行里使用python命令时,系统会从PATH环境变量里所设置的路径里查找python命令,而上面这段代码将 /home/test_env/cool/bin 放在PATH环境变量的最前面,这样,找到的python命令就是/home/test_env/cool/bin/python,找到的pip也正是/home/test_env/cool/bin/pip, 那么安装的第三方库也自然被安装在cool/lib/python3.7/site-packages中。
2.2 修改命令行提示符
修改命令行提示符的代码如下
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1-}"
if [ "x" != x ] ; then
PS1="${PS1-}"
else
PS1="(`basename \"$VIRTUAL_ENV\"`) ${PS1-}"
fi
export PS1
fi
这里的核心是PS1,它是一个环境变量,用来控制提示符的显示格式和内容,在shell里可以直接输出这个环境变量的值
[root@bffd06f7a14d test_env]# echo $PS1
[\u@\h \W]\$
[root@bffd06f7a14d test_env]#
PS1的常用参数以及含义:
- \d :代表日期,格式为weekday month date,例如:“Mon Aug 1”
- \H :完整的主机名称
- \h :仅取主机名中的第一个名字
- \t :显示时间为24小时格式,如:HH:MM:SS
- \T :显示时间为12小时格式
- \A :显示时间为24小时格式:HH:MM
- \u :当前用户的账号名称
- \v :BASH的版本信息
- \w :完整的工作目录名称
- \W :利用basename取得工作目录名称,只显示最后一个目录名
- \# :下达的第几个命令
- $ :提示字符,如果是root用户,提示符为 # ,普通用户则为 $
通过修改PS1的值,就可以修改提示符,比如我可以这样来操作
[root@bffd06f7a14d test_env]# PS1='(cool)[\h@\u \W \t]\$'
(cool)[bffd06f7a14d@root test_env 09:21:53]#
我对调了用户名和主机名的位置并增加了时间,在activate文件中,basename “$VIRTUAL_ENV” 其实就是执行basename /home/test_env/cool, 得到的结果是cool,经过字符串拼装,就修改了原来的提示符,在原有的提示符前面增加了(cool)
2.3 退出虚拟环境
退出虚拟环境,使用deactivate 命令,这个命令在activate文件中是一个函数实现的,其主要的任务是恢复之前的PS1,PYTHONHOME, PATH,这些环境变量在执行source activate命令时都保存了一份旧的。恢复以后,标志进入虚拟环境的提示符前缀也就没有了。
3. 总结
- 所谓虚拟环境,本质上就是一个python标准环境
- 进入虚拟环境,需要使用source命令,activate里的环境变量都会生效,修改PATH环境变量,修改命令行提示符,增加虚拟环境前缀,同时注册了deactivate函数
- 退出虚拟环境,正是调用之前注册的deactivate函数