new post: how-python-module-search-work-with-pyenv-and-pyenv-virtualenv.md
All checks were successful
ci/woodpecker/push/deploy Pipeline was successful
All checks were successful
ci/woodpecker/push/deploy Pipeline was successful
This commit is contained in:
parent
6d46404477
commit
9c0317aee6
|
@ -0,0 +1,245 @@
|
||||||
|
---
|
||||||
|
title: "Python 搜索模块的逻辑和 Pyenv 的工作逻辑"
|
||||||
|
date: 2023-04-12T21:58:50+08:00
|
||||||
|
tags: []
|
||||||
|
categories: []
|
||||||
|
weight: 50
|
||||||
|
show_comments: true
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
pyenv 是一款 Python 版本管理器,但是它不管理虚拟环境,所以不能像 conda 一样开箱即用地管理环境,但是 pyenv 搭配 pyenv-virtualenv 插件以后就具备了管理虚拟环境的能力。本篇文章对 pyenv-virtualenv 下 Python 的虚拟环境的工作方式进行简单介绍。
|
||||||
|
|
||||||
|
<!--more-->
|
||||||
|
|
||||||
|
在这里,我已经通过 pyenv 安装了 3.10.9 版本的 Python,并借助 pyenv-virtualenv 插件创建了一个名为 play 的虚拟环境。关于如何安装 pyenv 和 pyenv-virtualenv 以及创建环境请参考 [pyenv 的主页][1] 和 [pyenv-virtualenv 的主页][2]。
|
||||||
|
|
||||||
|
## Python 搜索模块的逻辑
|
||||||
|
|
||||||
|
> 本节的内容可以见 [Python 文档][3]。
|
||||||
|
|
||||||
|
首先在使用 `import` 时,Python 会从 `sys.path` 的列表所记录的路径中搜索模块,这和 GNU/Linux 环境下环境变量 `PATH` 的工作逻辑很像。所以虚拟环境的目的就是使运行中的 Python 的 `sys.path` 的内容与系统全局的 Python 环境进行区分,从而实现在不同的位置搜索模块和安装模块。
|
||||||
|
|
||||||
|
那么问题就变成了 `sys.path` 是怎样确定的,答案就在 [文档][3] 里面,简单汇总为以下:
|
||||||
|
|
||||||
|
### sys.path 的确定
|
||||||
|
|
||||||
|
如果有实际运行的 Python 脚本文件的话,比如 main.py,那么该脚本所在的目录就是 `sys.path` 的第一个条目,否则(比如使用 `python -c` 的方式运行命令)当前目录会是 `sys.path` 的第一个条目。
|
||||||
|
|
||||||
|
然后环境变量 `PYTHONPATH` 的值会被追加到 `sys.path` 之中,如果 `PYTHONPATH` 存在的话。
|
||||||
|
|
||||||
|
然后是添加依赖包含标准 Python 模块的目录和拓展模块(extension modules)的目录,这里与 `prefix` 和 `exec_prefix` 有关。
|
||||||
|
|
||||||
|
最后是添加 `site-packages`,它的路径是由 site 模块所生成的。
|
||||||
|
|
||||||
|
### prefix 和 exec_prefix 的确定
|
||||||
|
|
||||||
|
> 本节表述中 `X` 表示 Python 的主版本号,`Y` 表示 Python 的次版本号,例如对于 Python 3.11 来说,`X` 为 3,`Y` 为 11。
|
||||||
|
|
||||||
|
在 Linux 下 `${prefix}/lib/pythonX.Y` 下包含不依赖平台的标准 Python 模块,`${exec_prefix}/lib/pythonX.Y/lib-dynload` 下包含依赖于平台拓展模块(extension modules),所以这两个路径会添加到 `sys.path` 中去,因此需要先确定 `prefix` 和 `exec_prefix` 的值。
|
||||||
|
|
||||||
|
> 拓展模块(extension modules)是由 C 或 C++ 编写的模块,与用户代码通过 C API 交互,如 Windows 平台的 `.pyd` 文件和其他平台的 `.so` 文件
|
||||||
|
|
||||||
|
如果环境变量 `PYTHONHOME` 存在,那么 `prefix` 和 `exec_prefix` 会直接从变量中获取。
|
||||||
|
|
||||||
|
如果环境变量 `PYTHONHOME` 不存在,`prefix` 和 `exec_prefix` 会从 `home` 开始,逐级向上寻找“地标”文件和目录来确定。`home` 的默认值是包含 Python 二进制可执行文件的目录的真实路径(符号链接会被解引用,所以只有真实位置会作为起点)。
|
||||||
|
|
||||||
|
如果环境变量 `PYTHONHOME` 不存在,并且在 Python 二进制可执行文件相同目录或其上一级目录下存在 `pyvenv.cfg` 文件,那么 `home` 会遵守 `pyvenv.cfg` 中的配置,这和 Python 虚拟环境有关。
|
||||||
|
|
||||||
|
从 `home` 开始,`prefix` 会通过寻找 `pythonXY.zip` 来确定,在 Windows 上会在 `home` 直接查找,但是在 Unix 上会在 `lib/` 下查找。注意即便这个压缩文件完全找不到,也会最终添加到 `sys.path` 中,这个行为的解释在 [这里][4] 有提到。如果没有找到这个 .zip 文件,Windows 上会继续寻找 `Lib\os.py` 来确定 `prefix`,对应在 Unix 上会寻找 `lib/pythonX.Y/os.py` 。
|
||||||
|
|
||||||
|
在 Windows 上 `prefix` 和 `exec_prefix` 是相同的,但是其他平台上会继续搜索 `lib/pythonX.Y/lib-dynload` 来确定 `exec_prefix`。
|
||||||
|
|
||||||
|
从 `home` 搜索的过程类似如下,一般在第二次检查会成功,如果一直找不到“地标”文件,则会抛出错误:
|
||||||
|
|
||||||
|
```
|
||||||
|
Python executable path is /usr/bin/python3
|
||||||
|
check /usr/bin/lib/pythonX.Y/os.py -> if exists, prefix=/usr/bin
|
||||||
|
check /usr/lib/pythonX.Y/os.py -> if exists, prefix=/usr
|
||||||
|
check /lib/pythonX.Y/os.py -> if exists, prefix=/
|
||||||
|
```
|
||||||
|
|
||||||
|
### site-packages 的确定
|
||||||
|
|
||||||
|
在 `preifx` 和 `exec_prefix` 已经确定的情况下,使用 [site][5] 模块来生成 `site-packages` 的路径。
|
||||||
|
|
||||||
|
它的工作逻辑时,使用 `prefix` 和 `exec_prefix` 作为路径的前半部分,在 Windows 上使用 `lib/site-packages`、Linux 上使用 `lib/pythonX.Y/site-packages` 作为后半部分,分别组合得到两个路径,并忽略掉不存在的路径,存在的路径会添加到 `sys.path` 中。
|
||||||
|
|
||||||
|
## 开始我们的验证
|
||||||
|
|
||||||
|
验证时使用一个简单的程序打印信息,在我的环境中,它保存在 `~/Desktop/tmp/main.py`:
|
||||||
|
|
||||||
|
```
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("base_prefix:", sys.base_prefix)
|
||||||
|
print("base_exec_prefix:", sys.base_exec_prefix)
|
||||||
|
print("prefix:", sys.prefix)
|
||||||
|
print("exec_prefix:", sys.exec_prefix)
|
||||||
|
for i, j in enumerate(sys.path):
|
||||||
|
print("sys.path", i, ":", j)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
在系统全局下,程序输出如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ python ~/Desktop/tmp/main.py
|
||||||
|
base_prefix: /usr
|
||||||
|
base_exec_prefix: /usr
|
||||||
|
prefix: /usr
|
||||||
|
exec_prefix: /usr
|
||||||
|
sys.path 0 : /home/leafee98/Desktop/tmp
|
||||||
|
sys.path 1 : /usr/lib/python310.zip
|
||||||
|
sys.path 2 : /usr/lib/python3.10
|
||||||
|
sys.path 3 : /usr/lib/python3.10/lib-dynload
|
||||||
|
sys.path 4 : /usr/lib/python3.10/site-packages
|
||||||
|
```
|
||||||
|
|
||||||
|
在我的一个虚拟环境下,程序输出如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ python ~/Desktop/tmp/main.py
|
||||||
|
base_prefix: /home/leafee98/.pyenv/versions/3.10.9
|
||||||
|
base_exec_prefix: /home/leafee98/.pyenv/versions/3.10.9
|
||||||
|
prefix: /home/leafee98/.pyenv/versions/diffusion-webui
|
||||||
|
exec_prefix: /home/leafee98/.pyenv/versions/diffusion-webui
|
||||||
|
sys.path 0 : /home/leafee98/Desktop/tmp
|
||||||
|
sys.path 1 : /home/leafee98/.pyenv/versions/3.10.9/lib/python310.zip
|
||||||
|
sys.path 2 : /home/leafee98/.pyenv/versions/3.10.9/lib/python3.10
|
||||||
|
sys.path 3 : /home/leafee98/.pyenv/versions/3.10.9/lib/python3.10/lib-dynload
|
||||||
|
sys.path 4 : /home/leafee98/.pyenv/versions/diffusion-webui/lib/python3.10/site-packages
|
||||||
|
```
|
||||||
|
|
||||||
|
在系统全局环境下,通过设置环境变量 `PYTHONHOME` 来控制 `prefix` 和 `exec_prefix`,输出如下
|
||||||
|
|
||||||
|
```
|
||||||
|
$ PYTHONHOME=/home/leafee98/.pyenv/versions/3.10.9:/usr python ~/Desktop/tmp/main.py
|
||||||
|
base_prefix: /home/leafee98/.pyenv/versions/3.10.9
|
||||||
|
base_exec_prefix: /usr
|
||||||
|
prefix: /home/leafee98/.pyenv/versions/3.10.9
|
||||||
|
exec_prefix: /usr
|
||||||
|
sys.path 0 : /home/leafee98/Desktop/tmp
|
||||||
|
sys.path 1 : /home/leafee98/.pyenv/versions/3.10.9/lib/python310.zip
|
||||||
|
sys.path 2 : /home/leafee98/.pyenv/versions/3.10.9/lib/python3.10
|
||||||
|
sys.path 3 : /usr/lib/python3.10/lib-dynload
|
||||||
|
sys.path 4 : /home/leafee98/.pyenv/versions/3.10.9/lib/python3.10/site-packages
|
||||||
|
sys.path 5 : /usr/lib/python3.10/site-packages
|
||||||
|
```
|
||||||
|
|
||||||
|
从上面的例子,可以看到 `sys.path` 的第一个元素就是脚本所在的位置。从第三个例子中看到标准 Python 模块的路径是借助 `prefix` 设置的,而拓展模块是借助 `exec_prefix` 设置的,最后将 `prefix` 和 `exec_prefix` 分别和 `lib/pythonX.Y/site-packages` 结合,得到两个路径并且这两个路径全部存在,所以全部添加到了 `sys.path` 中。
|
||||||
|
|
||||||
|
## pyenv 下的工作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
$ python ~/Desktop/tmp/main.py
|
||||||
|
base_prefix: /home/leafee98/.pyenv/versions/3.10.9
|
||||||
|
base_exec_prefix: /home/leafee98/.pyenv/versions/3.10.9
|
||||||
|
prefix: /home/leafee98/.pyenv/versions/diffusion-webui
|
||||||
|
exec_prefix: /home/leafee98/.pyenv/versions/diffusion-webui
|
||||||
|
sys.path 0 : /home/leafee98/Desktop/tmp
|
||||||
|
sys.path 1 : /home/leafee98/.pyenv/versions/3.10.9/lib/python310.zip
|
||||||
|
sys.path 2 : /home/leafee98/.pyenv/versions/3.10.9/lib/python3.10
|
||||||
|
sys.path 3 : /home/leafee98/.pyenv/versions/3.10.9/lib/python3.10/lib-dynload
|
||||||
|
sys.path 4 : /home/leafee98/.pyenv/versions/diffusion-webui/lib/python3.10/site-packages
|
||||||
|
```
|
||||||
|
|
||||||
|
在上面的输出中,虚拟环境所使用的工具就是 pyenv,所以本节将介绍这个输出中 `sys.path` 中每一个值的来源。
|
||||||
|
|
||||||
|
`sys.path` 中的 0 就是脚本所在的位置。
|
||||||
|
|
||||||
|
在虚拟环境下, pyenv 将 `/home/leafee98/.pyenv/plugins/pyenv-virtualenv/shims` 和 `/home/leafee98/.pyenv/shims` 添加到 `PATH` 中,于是当敲出 `python` 时,实际运行的是下面这样的一个脚本
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cat /home/leafee98/.pyenv/shims/python
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
[ -n "$PYENV_DEBUG" ] && set -x
|
||||||
|
|
||||||
|
program="${0##*/}"
|
||||||
|
|
||||||
|
export PYENV_ROOT="/home/leafee98/.pyenv"
|
||||||
|
exec "/usr/share/pyenv/libexec/pyenv" exec "$program" "$@"
|
||||||
|
```
|
||||||
|
|
||||||
|
不想分析脚本运行逻辑了,使用 `PYENV_DEBUG=1 python3` 可以看到 pyenv 所输出的调试信息(部分输出如下),其中可以得知最后运行的二进制 Python 的路径是 `/home/leafee98/.pyenv/versions/diffusion-webui/bin/python3`,于是就回到了上面初始化 Python `sys.path` 的流程。
|
||||||
|
|
||||||
|
```
|
||||||
|
$ PYENV_DEBUG=1 python ~/Desktop/tmp/main.py
|
||||||
|
+ program=python
|
||||||
|
+ export PYENV_ROOT=/home/leafee98/.pyenv
|
||||||
|
+ PYENV_ROOT=/home/leafee98/.pyenv
|
||||||
|
+ exec /usr/share/pyenv/libexec/pyenv exec python /home/leafee98/Desktop/tmp/main.py
|
||||||
|
+(/usr/share/pyenv/libexec/pyenv:23): enable -f /usr/share/pyenv/libexec/../libexec/pyenv-realpath.dylib
|
||||||
|
......
|
||||||
|
+(/usr/share/pyenv/libexec/pyenv-exec:46): PATH=/home/leafee98/.pyenv/versions/diffusion-webui/bin:/usr/share/pyenv/libexec:/home/leafee98/.pyenv/plugins/pyenv-virtualenv/bin:/usr/share/pyenv/plugins/python-build/bin:/home/leafee98/.pyenv/plugins/pyenv-virtualenv/shims:/home/leafee98/.pyenv/shims:/usr/local/sbin:/usr/local/bin:/usr/bin:/opt/cuda/bin:/opt/cuda/nsight_compute:/opt/cuda/nsight_systems/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/home/leafee98/.local/bin:/home/leafee98/.yarn/bin:/home/leafee98/go/bin
|
||||||
|
+(/usr/share/pyenv/libexec/pyenv-exec:48): exec /home/leafee98/.pyenv/versions/diffusion-webui/bin/python /home/leafee98/Desktop/tmp/main.py
|
||||||
|
......
|
||||||
|
```
|
||||||
|
|
||||||
|
首先 `/home/leafee98/.pyenv/versions/diffusion-webui/bin/python3` 的上一级存在 `pyvenv.cfg`,它的内容如下
|
||||||
|
|
||||||
|
```
|
||||||
|
$ cat /home/leafee98/.pyenv/versions/diffusion-webui/bin/../pyvenv.cfg
|
||||||
|
home = /home/leafee98/.pyenv/versions/3.10.9/bin
|
||||||
|
include-system-site-packages = false
|
||||||
|
version = 3.10.9
|
||||||
|
```
|
||||||
|
|
||||||
|
所以遵循该虚拟环境的配置,`/home/leafee98/.pyenv/versions/3.10.9/bin` 作为 home 开始寻找地标文件 `lib/python310.zip` 和 `lib/python3.10/os.py`,以此来确定 `prefix`。
|
||||||
|
|
||||||
|
```
|
||||||
|
$ echo_if_exist() { [[ -e "$1" ]] && echo "$1 exist" ; }
|
||||||
|
$ home=/home/leafee98/.pyenv/versions/3.10.9/bin
|
||||||
|
$ echo_if_exist ${home}/lib/python3.10.zip
|
||||||
|
$ echo_if_exist ${home}/lib/python3.10/os.py
|
||||||
|
$ home=$(dirname $home)
|
||||||
|
$ echo_if_exist ${home}/lib/python3.10.zip
|
||||||
|
$ echo_if_exist ${home}/lib/python3.10/os.py
|
||||||
|
/home/leafee98/.pyenv/versions/3.10.9/lib/python3.10/os.py exist
|
||||||
|
```
|
||||||
|
|
||||||
|
`/home/leafee98/.pyenv/versions/3.10.9/lib/python3.10/os.py` 存在,所以 `prefix` 确定为 `home/leafee98/.pyenv/versions/3.10.9`。
|
||||||
|
|
||||||
|
重新从 home 开始寻找地标文件 `lib/python3.10/lib-dynload` 来确定 `exec_prefix`。
|
||||||
|
|
||||||
|
```
|
||||||
|
$ home=/home/leafee98/.pyenv/versions/3.10.9/bin
|
||||||
|
$ echo_if_exist ${home}/lib/python3.10/lib-dynload
|
||||||
|
$ home=$(dirname $home)
|
||||||
|
$ echo_if_exist ${home}/lib/python3.10/lib-dynload
|
||||||
|
/home/leafee98/.pyenv/versions/3.10.9/lib/python3.10/lib-dynload exist
|
||||||
|
```
|
||||||
|
|
||||||
|
`/home/leafee98/.pyenv/versions/3.10.9/lib/python3.10/lib-dynload` 存在,所以 `exec_prefix` 确定为 `/home/leafee98/.pyenv/versions/3.10.9`。
|
||||||
|
|
||||||
|
于是根据 `prefix` 和 `exec_prefix` 添加标准模块和拓展模块的路径到 `sys.path`,即虚拟环境的输出中 `sys.path` 中的 1、2、3。
|
||||||
|
|
||||||
|
然后是 site 模块来添加 `site-packages` 到 `sys.path`,但是在虚拟环境中,`prefix` 和 `exec_prefix` 会指向虚拟环境的路径,而不是通过地标文件所找到的路径:
|
||||||
|
|
||||||
|
> When a Python interpreter is running from a virtual environment, sys.prefix and sys.exec_prefix point to the directories of the virtual environment, whereas sys.base_prefix and sys.base_exec_prefix point to those of the base Python used to create the environment. It is sufficient to check sys.prefix == sys.base_prefix to determine if the current interpreter is running from a virtual environment.
|
||||||
|
>
|
||||||
|
> [Source][6]
|
||||||
|
|
||||||
|
在这里即指向 `/home/leafee98/.pyenv/versions/diffusion-webui`,于是分别拼接 `lib/python3.10/site-packages` 并去除不存在的路径和重复的路径,得到 `/home/leafee98/.pyenv/versions/diffusion-webui/lib/python3.10/site-packages`,即虚拟环境的输出中 `sys.path` 中的 5。
|
||||||
|
|
||||||
|
## 参考:
|
||||||
|
|
||||||
|
1. https://github.com/pyenv/pyenv
|
||||||
|
2. https://github.com/pyenv/pyenv-virtualenv
|
||||||
|
3. https://docs.python.org/3/library/sys_path_init.html#sys-path-init
|
||||||
|
4. https://stackoverflow.com/questions/34822593/why-does-sys-path-have-c-windows-system-python34-zip/49293544#49293544
|
||||||
|
5. https://docs.python.org/3/library/site.html#module-site
|
||||||
|
6. https://docs.python.org/3/library/venv.html#how-venvs-work
|
||||||
|
|
||||||
|
[1]: https://github.com/pyenv/pyenv
|
||||||
|
[2]: https://github.com/pyenv/pyenv-virtualenv
|
||||||
|
[3]: https://docs.python.org/3/library/sys_path_init.html#sys-path-init
|
||||||
|
[4]: https://stackoverflow.com/questions/34822593/why-does-sys-path-have-c-windows-system-python34-zip/49293544#49293544
|
||||||
|
[5]: https://docs.python.org/3/library/site.html#module-site
|
||||||
|
[6]: https://docs.python.org/3/library/venv.html#how-venvs-work
|
Loading…
Reference in a new issue