使用 pip 把依赖一股脑儿装在全局环境下有些隐患:

  1. 依赖冲突:如果项目 A 依赖 dep1.0,项目 B 依赖 dep2.0,无论安装 dep1.0 还是 dep2.0,都将导致其中一个项目无法运行。
  2. 依赖冗余:当我们使用 pip freeze 将依赖快照到 requirements.txt 时,得到的,不是当前项目的真实依赖,而是本地项目的所有依赖。

venv 通过为每个项目创建一个干净的虚拟环境很好地解决了这些问题。

venv

目录

1. 使用 venv 隔离 Python 依赖

1.1. 创建虚拟环境

venv 是 Python3.6+ 的内置模块。

使用 venv 创建虚拟环境非常简单,假设我们的项目名为 friday,只需要进我们的项目目录,执行:

➜ cd friday
➜ python -m venv .venv

最后一个参数 .venv 指定了虚拟环境的位置。

当前项目目录下就会出现一个 .venv 文件夹,它的结构长这样:

├── .venv
│   ├── bin
│   │   ├── activate
│   │   ├── pip
│   │   ├── python
│   │   └── ...
│   ├── lib
│   │   ├── python3.8
│   │   │   └── site-packages
│   │   │       └── ...
│   │   └── ...
│   └── pyvenv.cfg
│   └── ...
├── main.py

这,就是当前项目的虚拟环境。

1.2. 使用虚拟环境

创建虚拟环境后,Python 并不会聪明地自动使用该虚拟环境,我们还需要主动进入其中:

➜ source .venv/bin/activate
(.venv) ➜  

当我们的命令行有 (.venv) 前缀时,说明我们在虚拟环境里了。

接下来,我们可以在虚拟环境里,像之前那样,使用 pip 安装依赖:

(.venv) ➜ pip install requests

此时,requests 会被安装到 .venv/lib/python3.8/site-packages 目录下,而不是之前的全局环境中。

当我们想分享项目时,执行:

(.venv) ➜ pip freeze > requirements.txt

当前项目的真实依赖就会被快照到当前目录下的 requirements.txt 里。

协作者在拿到我们的源码后,只需要安装 requirements.txt 里声明的依赖,即可在他的本地复现我们的开发环境了:

➜ source .venv/bin/activate
(.venv) ➜ pip install -r requirements.txt

如果想退出虚拟环境,执行:

(.venv) ➜ deactivate
➜

2. venv 的工作原理

2.1. 认识 Python 环境

在了解 venv 的工作原理前,我们先认识下 Python 环境。

Unix 世界里,应用环境的布局通常如下:

├── env
│   ├── bin
│   ├── lib
│   ├── share
│   └── ...

其中 bin 目录用来存放可执行文件,lib 目录用来存放执行时用到库文件。

检查一下我们的 /usr「系统应用」,我们的 /usr/local「用户应用」,它们都是如此。

如果我们在用系统自带的 Python,我们的 Python 环境就是 /usr,如果我们在用 homebrew 安装的 Python,我们的 Python 环境就是 /usr/local

├── /usr # or /usr/local or /other location
│   ├── bin
│   │   ├── pip
│   │   ├── python
│   │   ├── pip3
│   │   ├── python3
│   │   └── ...
│   ├── lib
│   │   ├── python3.8
│   │   │   └── site-packages   # pip 安装的依赖
│   │   │       └── requests
│   │   │       └── ...
│   │   └── ...
│   └── ...

2.2. Python 是怎么找包的?

Python 遍历 sys.path 的值找包,sys.path 是一个路径列表,其默认值通常如下:

➜ python
>>> import sys
>>> print('\n'.join(sys.path))

/Users/xq.jin/.pyenv/versions/3.8.0/lib/python38.zip
/Users/xq.jin/.pyenv/versions/3.8.0/lib/python3.8
/Users/xq.jin/.pyenv/versions/3.8.0/lib/python3.8/lib-dynload
/Users/xq.jin/.pyenv/versions/3.8.0/lib/python3.8/site-packages

其中,第一项是空字符串,指当前目录,最后一项是 site-packages,指 pip 安装依赖的目录。

当 Python 遍历完 sys.path 还是找不到要 import 的包时,就会 ModuleNotFound: No module named 'x'

sys.path 的值与其它几个值有关:

  1. sys.executable,当解释器开始执行时,我们就有了解释器的位置,解释器会把该值给 sys.executable
  2. sys.prefix,我们沿着 sys.executable 往上找,很容易找到 /Users/xq.jin/.pyenv/versions/3.8.0,它有 bin,有 lib,显然,这就是 python 的运行环境,解释器会把该值给 sys.prefix
  3. 这是,解释器就可以把 {sys.prefix}/lib/pythonx.y/site-packages 加到 sys.path 里了,其中,x.y 即当前 Python 版本 3.8。pip 会把依赖安装到这里。

2.3. venv 是怎么工作的?

有了以上两点知识储备,venv 的工作原理就显而易见了。

首先,venv 在当前工作目录下,创建了一套完整的 Python 环境,即上文中的 .venv 目录。

当我们使用 source .venv/bin/activate 时,它把 .venv/bin 插在了 $PATH 的开头。当我们使用 pythonpip 等命令时,调用的,其实都是 .venv/bin 下的可执行文件。

在虚拟环境里输出 $PATH,可以看到,.venv/bin 在最前边。

(.venv) ➜ ruby -e "puts %x(echo $PATH).split(':')"

/Users/xq.jin/Desktop/py-venv/.venv/bin
...
/usr/local/bin
/usr/bin
...

那么,在虚拟环境里,sys.executable 的值将是 $(pwd)/.venv/bin/pythonsys.prefix 自然就是 $(pwd) 了;sys.path 里有 $(pwd)/.venv/lib/python3.8/site-packages 也就水到渠成。