目录
  1. 前言
    1. 问题
  2. virtualenv
    1. 基础流程
    2. 激活虚拟环境
      1. 主流程代码
      2. 1. 退出虚拟环境
      3. 2. 设置VIRTUAL_ENV和PATH
      4. 3. 销毁PYTHONHOME变量
      5. 4. 修改终端提示符显示
      6. 5. 取消pydoc的别名并设置新的pydoc
      7. 6. 干掉已经缓存的$PATH
      8. 退出虚拟环境
  3. 最后
  4. 参考
virtualenv虚拟环境原理

前言

用过几次python的虚拟环境,感觉非常爽,一直想去了解它的原理,于是借此机会,做了一次源码学习,但由于conda的源码比较复杂,于是此文便以virtualenv作为切入点。

问题

在之前,我一直有如下几个疑惑,在此先抛出来

  1. python是怎么决定默认包搜索路径?
  2. 激活虚拟环境与直接调用虚拟环境中的python有什么区别?
  3. 虚拟环境是如何修改终端提示的?
  4. conda和pip安装的包在同一位置吗?
  5. conda与virtualenv虚拟环境的优先级?

virtualenv

基础流程

1
2
3
4
1. 安装virtualenv: pip install virtualenv
2. 创建虚拟环境: virtualenv --no-site-packages venv
3. 激活虚拟环境: source venv/bin/activate
4. 退出虚拟环境: deactivate

激活虚拟环境

激活虚拟环境通过source venv/bin/activate命令来搞的,那么我们的切入点就是venv/bin/activate这个shell脚本。

直接来阅读源码,

主流程代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 退出虚拟环境
deactivate nondestructive

# 设置VIRTUAL_ENV为venv目录
VIRTUAL_ENV="/Users/hongshu/tmp/venv"
export VIRTUAL_ENV

# 将VIRTUAL_ENV添加到PATH的最前面
_OLD_VIRTUAL_PATH="$PATH" # 保留原先的PATH变量
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH

# 销毁PYTHONHOME变量
if ! [ -z "${PYTHONHOME+_}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="$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

# 取消pydoc的别名
alias pydoc 2>/dev/null >/dev/null && unalias pydoc || true

# 设置新的pydoc
pydoc () {
python -m pydoc "$@"
}

# 干掉已经缓存的$PATH
if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ] ; then
hash -r 2>/dev/null
fi

1. 退出虚拟环境

这一步中执行了一条deactivate命令,deactivate其实是一个函数,同时如果仔细想想我们可以发现当我们执行销毁虚拟环境的时候其实就是执行的deactiavte。这两个deactivate就是同一个东西,如果说唯一的区别就是这里多了一个nondestructive作为参数。这个由于与创建的过程强相关,放到后面退出虚拟环境部分讲。

2. 设置VIRTUAL_ENV和PATH

这一步可以说是虚拟环境的核心吧,就是将venv添加到PATH中去。

疑问 而且我们可以看到此处包括后面却没有将虚拟环境中的依赖库的位置包含进去,那这又是为何呢?

3. 销毁PYTHONHOME变量

这一步有个特别有趣的东西,可以看到${PYTHONHOME+_}后面有一个+_,这个地方应该是叫做“shell参数扩展”。

此处${a+B}表示如果a不为空,则返回”B”。

在下面的脚本中还有${a-B},表示如果a未定义或为空,则返回”B”。

更多的可以参考Shell Bash 中的参数扩展

4. 修改终端提示符显示

我们每次在激活虚拟环境之后,命令行的显示都会发生变化,如下图

image-20190227005558061

就是在上方会多出一个关于虚拟环境目录的提示。根据代码,我们也很容易发现是通过对PS1变量的修改来触发的,同样也可以发现如何隐藏掉这个提示,设置环境变量VIRTUAL_ENV_DISABLE_PROMPT即可

image-20190227005905253

如上图所示,设置环境变量VIRTUAL_ENV_DISABLE_PROMPT之后,venv的提示就不见了,销毁掉该环境变量后就又出来了。

5. 取消pydoc的别名并设置新的pydoc

这一步的意思就是用新的pydoc取代原先的pydoc,至于要取消别名是因为有别名的前提下无法定义函数。下面做个简单的实验。先定义别名再定义函数。

image-20190227213725487

可以看到存在别名后函数定义错误。但是先定义函数再定义别名却是可以的,这种时候别名会优先被使用,如下图所示

image-20190227213952281

6. 干掉已经缓存的$PATH

因为即使我们设置了新的PATH路径,但是有些不在新的PATH路径的命令会因为缓存原因而被依然能够被执行。这一步没有实验成功,姑且认为可能会存在这种情况吧。

退出虚拟环境

退出虚拟环境调用的是deactivate命令,其实就是在source venv/bin/activate脚本中创建的一个函数而已,如下图所示

image-20190227220509106

那么结合创建虚拟环境的过程,就可以发现此处做的工作无非就是将几个变量(PATH、PYTHONHOME、PS1)重新恢复了而已。真正需要关注的只有最后一步,最后一步中我们可以看到里面对传参nondestructive做了处理,如果传入了nondestructive,那么就不会销毁到deactivate函数,否则销毁。

最后

最开始留了几个问题,虽然有几个无法从上文中找到答案,但憋着总是很难受,就通过实验的方式揭秘这些问题的答案。

1. python是怎么决定默认包搜索路径

在整个激活虚拟环境的过程中,我发现并没有任何对python的包路径进行设置的语句,于是此处产生了两个猜想:

  1. python的默认包搜索路径在python安装的时候就已经定好了
  2. python的默认包搜索路径由python所在位置决定

接下来就是验证哪个猜想是正确的了,于是做了个简单的实验,找一个虚拟环境,将其原封不动地拷贝到一个新的目录下,

image-20190301000409284

拷贝完成后执行python,发现报错,但是根据报错信息,可以很大概率上觉得猜想2更接近于事实。

那么接下来,解决掉报错信息,继续执行下去

image-20190301000851825

可以看到最终结果表明猜想2才是正确的。

2. 激活虚拟环境与直接调用虚拟环境中的python有什么区别

从上述虚拟环境激活脚本中可以看到直接调用虚拟环境中的python几乎没有太大区别,真正对运行可能产生影响的是PYTHONHOME变量。

3. 虚拟环境是如何修改终端提示的

这个问题在激活虚拟环境的第4步我们其实已经得到了答案,PS1变量决定了终端的提示信息。至于隐藏虚拟环境的终端提示,只需要在.bashrc这类的文件中设置VIRTUAL_ENV_DISABLE_PROMPT环境变量便可以关掉。甚至我们还可以通过VIRTUAL_ENV和PS1变量来自定义自己的终端提示。(ps: 文章中截图中的终端提示python3.7就是利用conda环境变量做的自定义终端提示)

4. conda和pip安装的包在同一位置吗

此处测试了一下,的确是安装在同样的目录下的,不过conda应该是通过conda-meta目录来记录自己的数据的。

image-20190228230730600

5. conda与virtualenv虚拟环境的优先级

image-20190301001404636

做了个简单实验,可以发现后执行的会覆盖掉先执行的。

参考

[1] Shell Bash 中的参数扩展

文章作者: 谷和阿秋
文章链接: https://www.lyytaw.com/python/python%E8%99%9A%E6%8B%9F%E7%8E%AF%E5%A2%83%E5%8E%9F%E7%90%86/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 谷和阿秋|BLOG