Python3 的虚拟环境是怎样工作的呢?以前我从来没想过这个问题。。。
先来做个简单的实验
第一个小实验
将 python3 binary 拷贝到任意一个目录(比如 /tmp/venv_test),然后执行它
(下面代码均在 linux 进行测试,在 Windows 和 OSX 上运行可能会出错)
#code:
rm -rf /tmp/venv_test # 谨慎使用
venvPath="/tmp/venv_test/"
venvBinPath="$venvPath/bin"
mkdir -p $venvBinPath
pythonPath=`which python3`
cp $pythonPath $venvBinPath
"$venvBinPath/python3" -c "import time; print ('{} we can use time library'.format(time.time()))"
#output:
1517129241.1597247 we can use time library
我们发现,把 python 可执行文件随意拷贝到一个目录,它都可以正常执行。 这个是符合我们预期的。比如我们写一个 c 语言的 hello world 程序, 我们编译之后,把它丢在系统任意一个目录,它都可以执行。Python 解释器 也是 c 写的嘛。
第二个小实验
我们在 venv_test 目录创建 /usr/local/lib/python3.6/os.py 的软链接
#code:
rm -rf /tmp/venv_test/lib
venvPath="/tmp/venv_test"
venvBinPath="$venvPath/bin"
touch $venvPath/os.py
"$venvBinPath/python3" -c "import sys; print(sys.prefix)"
venvLibPath="$venvPath/lib/python3.6"
mkdir -p $venvLibPath
python3Prefix=$(python3 -c "import sys; print(sys.prefix)")
ospyPath="$python3Prefix/lib/python3.6/os.py"
ln -s $ospyPath "$venvLibPath/os.py"
$venvBinPath/python3 -c "import sys; print(sys.prefix)"
#output:
Fatal Python error: Py_Initialize: Unable to get the locale encoding
ModuleNotFoundError: No module named 'encodings'
Current thread 0x00007fb097b72700 (most recent call first):
bash: line 9: 9625 Aborted $venvBinPath/python3 -c "import sys; print(sys.prefix)"
从运行的输出可以看到,我们在 venv_test 目录下创建了 lib/python3.6/os.py 后, Python 就运行失败了。报错信息是说找不到 encodings 模块。熟悉 Python 的读者可能 知道 encodings 是标准库啊,为什么会找不到呢?我们只不过是创建了一个软链接嘛? 于是我 Google/Baidu 了一把,没找到相关资料。
lib/python3.6/os.py 这个文件有什么黑魔法么??请看下文。
神奇的 lib/pythonX.Y/os.py
既然 Google 搜不到相关问题,我们只能自己动手了。
使用代码搜索工具在 cpython 项目中 grep 一下 os.py,我们可以发现 Modules/getpath.c 文件中有相关描述,打开这个文件,我们可以看到文件第一行 写着『Return the initial module search path』,看起来这东西会影响 Python 的 import 机制,我们能不能从中找到 encoding 模块 import 失败的原因呢?
继续看这个文件的注释,它告诉我们它是怎样去找初始的模块搜索路径的呢? (假设没有设置 PYTHONHOME 环境变量)首先将 python executable 所在目录设为 argv0_path
然后从 argv0_path 目录以及它的父(父,父父…一直回溯)目录下去找 prefix 和 exec_prefix 目录。这两个目录各自有个特征, prefix 目录下一定存在文件: lib/python$VERSION/os.py 。 而 execprefix 目录下肯定存在目录: lib/python$VERSION/lib-dynload 。
//lib/python$VERSION 就是初始的模块搜索路径
举个?:对于系统上安装的 Python 而言,它的 prefix, execprefix 目录是 怎样的呢?它的初始模块搜索路径又是哪个呢?我们再做个试验。
#code:
prefix=$(python -c "import sys; print(sys.prefix)")
echo "Python prefix directory is $prefix"
# So, Initial Module search path contains $prefix/lib/python3.6
# Ok, Let's see what in $prefix/lib/python3.6
version=$(python -V 2>&1 | cut -d ' ' -f2 | cut -d'.' -f1-2)
ls -l $prefix/lib/python$version/ | head -n 10
[ -f $prefix/lib/python$version/os.py ] && echo "Yeah, os.py exists."
#output:
Python prefix directory is /usr/local
total 12440
-rw-r--r-- 1 root root 18415 Feb 17 2016 _abcoll.py
-rw-r--r-- 1 root root 25980 Feb 17 2016 _abcoll.pyc
-rw-r--r-- 1 root root 25980 Feb 17 2016 _abcoll.pyo
-rw-r--r-- 1 root root 7145 Feb 17 2016 abc.py
-rw-r--r-- 1 root root 6187 Feb 17 2016 abc.pyc
-rw-r--r-- 1 root root 6131 Feb 17 2016 abc.pyo
-rw-r--r-- 1 root root 34231 Feb 17 2016 aifc.py
-rw-r--r-- 1 root root 30745 Feb 17 2016 aifc.pyc
-rw-r--r-- 1 root root 30745 Feb 17 2016 aifc.pyo
Yeah, os.py exists.
我们可以在 //lib/python$VERSION 目录下看到很多标准库 的身影。我们是不是可以这样理解,Python 标准库都放在这个目录吧?(差不多是这样吧) 但是好像没找到我们自己安装的包呀。当我们使用 pip 或者 easyinstall 安装一个包的时候, 它会安装到哪个目录呢?这个问题留给读者自己研究,嘿嘿?。
从上面的分析,我们可以得出一个结论:Python 在启动时,会根据一些约定来 确认 prefix/execprefix 分别对应哪个目录,接着找到初始的模块搜索路径, 还有用户安装包的路径。然后Python 可以 import 这些路径下的包或者模块。
回到现象
按照我们在上面得出来的结论,我们可以大胆推断:当我们在 venvtest 目录创建 lib/python3.6/os.py 的软链时,Python 会把 lib/python3.6 这个目录作为 初始的模块搜索路径,但是我们这个目录下什么包、什么模块也没有。所以找不到 encodings 模块也是理所当然啊。
我们把 encodings 模块也软链到 lib/python3.6 下。
#code:
rm -rf /tmp/venv_test/lib
venvPath="/tmp/venv_test"
venvBinPath="$venvPath/bin"
venvLibPath="$venvPath/lib/python3.6"
mkdir -p $venvLibPath
python3Prefix=$(python3 -c "import sys; print(sys.prefix)")
ospyPath="$python3Prefix/lib/python3.6/os.py"
encodingPath="$python3Prefix/lib/python3.6/encodings"
ln -s "$encodingPath/" "$venvLibPath/"
ln -s $ospyPath "$venvLibPath/os.py"
$venvBinPath/python3 -c "import sys; print(sys.prefix)"
#output:
Fatal Python error: Py_Initialize: Unable to get the locale encoding
Traceback (most recent call last):
File "/tmp/venv_test/lib/python3.6/encodings/__init__.py", line 31, in
ModuleNotFoundError: No module named 'codecs'
bash: line 12: 30266 Aborted $venvBinPath/python3 -c "import sys; print(sys.prefix)"
现在是说找不到 codecs 模块了,说明我们把 encodings 目录软链接过来之后, Python 就可以找到这个模块了,证实了我们上面的推测。
正题:venv 是怎样工作的?
( 我们这里的 venv 泛指 Python 虚拟环境 ) 我们先看 Python3.6 标准库中的 venv 是怎样实现的,翻阅官方文档即可。 文档让我们去看 PEP 405,大家可以去细读一把。这个文档里面说了几个方面:虚拟环境是干啥用的?
为什么要在标准库里面搞个 venv?我们不是有 virutalenv 等一系列库了吗。
官方是怎样实现 venv 的?
它是怎样向后兼容的。
我这里大概的总结下:venv 可以创建一个轻量级的虚拟环境,这个虚拟环境 有自己的 site directory,可以与系统的隔离开来。其中,每个虚拟环境中 都有属于该环境的 Python Binary 和一系列包,它和系统中的 Python 共享 标准库。
由于 Python 以前没有好的内置相关机制支持虚拟环境,所以 virtualnev 等库实现起来都很糟心。就拿 virtualenv 来说,它把所有的标准库都软链接 到虚拟环境中,还修改了 site 模块。(在上面的实验中,我们也有把 encodings 模块 软链接到我们的 venvtest 目录下。)
上面我们也从 getpath.c 文件的注释中知道了以前 Python 是怎样确认 初始的模块搜索路径的。这个 PEP 提出一种新的『初始模块搜索』方法: 启动时,python 先去检查当前目录或者(父,父父…)等目录中是否存在一个 叫做 pyvenv.cfg 的文件,这个文件里面如果存在一个 key 叫 home , Python 解释器就把这当成一个虚拟环境(home key 的值就是系统中 Python 的 prefix 对应的目录)。 这时,虚拟环境中的 Python 会把 prefix 对应目录设为 pyvenv.cfg 所在目录。 把 baseprefix 设为系统中 Python 的 prefix 目录。虚拟环境中 Python 的初始模块搜索路径会加上 baseprefix 对应目录。O 了,虚拟环境 就这么简单。以后,我们往 prefix/lib/python$version/site-packages 安装包就好了,虚拟环境中的 Python 就能识别出来,可以进行 import。
所以简单来说,一个 Python Binary + pyvenv.cfg 文件就标识了一个虚拟 环境。我们可以在这环境里面搞事情,不会影响到系统 Python,也可以 防止被系统影响,比如包冲突啥的。
总结
简单大概的总结下:Python 虚拟环境的理论基础就是它的 prefix 确认机制。 它把哪个目录当成它的 prefix,哪就是他的工作环境。其它的模块 查找机制,很大程度上都是基于 prefix 这个东西来的。
Author: cosven
Created: 2018-01-29 Mon 08:42
Emacs 25.2.1 (Org mode 8.2.10)