AI 技术博客
返回首页
Python 基础 14 分钟阅读

虚拟环境 venv:依赖隔离的正确姿势

Python虚拟环境venv依赖管理

引言

前三篇我们完成了:为什么学 Python(第 1 篇)、装好解释器(第 2 篇)、理解一行代码的完整旅程(第 3 篇)。现在你已经能跑代码了,但作为工程师,很快就会撞上一个真问题:项目 A 需要 requests 2.28,项目 B 需要 requests 2.31,升级谁的? 如果没有隔离机制,答案只能是「谁先装谁赢」——然后某个项目的功能悄悄坏掉。这一篇的解决方案叫虚拟环境(virtual environment),标准库自带的工具叫 venv。它是 Python 工程化的第一块基石,也是后续第 5 篇(包管理)、第 9 篇(VS Code 配置)的学习前提。

读者应先掌握第 2、3 篇的概念:解释器(interpreter)、pip 安装包的位置(site-packages 目录,即存放第三方包的标准位置)。本篇目标:理解 venv 的隔离原理(为什么它轻量、为什么它不复制解释器),并在自己的机器上完成「创建 → 激活 → 装包 → 退出 → 删除」的完整闭环。

概念与原理

问题的根源:一个全局的 site-packages

第 2 篇我们确认过,pip 安装的包默认落在解释器的 site-packages 目录(Windows 上如 C:\Program Files\Python312\Lib\site-packages)。当你只有一个 Python、又直接往里面装包,这个目录就成了全局共享池:所有项目共用同一份依赖。两个项目对同一包的版本要求冲突时(requests 2.28 与 2.31 的 API 就有差异),谁后装谁覆盖,先前的项目就此进入「也许坏了」的不确定状态——这就是著名的「依赖地狱」(dependency hell)雏形。

venv 的核心思想:给每个项目一个专属的 site-packages

venv 的解决方案直截了当:为项目创建一个独立目录(惯例叫 .venv),里面放一套属于自己的解释器入口和 site-packages。效果是:

  • 在这个环境里安装的包,只进入 .venv 自己的 site-packages,不污染全局,也不被全局的包污染;
  • 不同项目各装各的版本,互不干扰;
  • 环境坏了直接删目录重建,五秒钟的事。

关键理解:venv 并不复制解释器本体。创建 venv 时,Python 只做了三件事:一是把 python 可执行文件的入口放进环境目录(Windows 上是 Scripts\python.exe,POSIX 上是 bin/python;实现上既可能是符号链接也可能是小体积的启动器副本,取决于平台);二是在环境根目录写一个 pyvenv.cfg 配置文件,记录基础解释器(base interpreter)的位置与版本;三是按需建立独立的 site-packages 目录。

pyvenv.cfg 就是解开一切疑惑的钥匙,本机(Windows,Python 3.12)创建后的真实内容:

home = C:\Program Files\Python312
include-system-site-packages = false
version = 3.12.10
executable = C:\Program Files\Python312\python.exe
command = C:\Program Files\Python312\python.exe -m venv <你的环境路径>

逐字段解读:home 与 executable 指向基础解释器——环境里的 python 最终仍由这个基础解释器执行代码,所以字节码、标准库都复用系统的,环境只隔离第三方包;version = 3.12.10 是严格约束,解释器启动时会校验它与当前可执行文件的次版本(minor version)是否一致,不一致就直接拒绝启动——这就是为什么「复制 venv 到另一台装不同版本 Python 的机器」会失败;include-system-site-packages = false 表示不与全局包互通(若创建时加了 --system-site-packages 选项则为 true,通常不需要)。

「激活」的真相:只是改 PATH

新手最大的认知门槛是激活(activate)。直觉上「激活」像是进入了某种魔法空间,其实它的全部工作就是:把环境里的可执行文件目录插到 PATH 的最前面,并设置 VIRTUAL_ENV 环境变量。也就是说,激活之后你再敲 python,PATH 查找的第一个命中就是 .venv 里的解释器——仅此而已。这解释了三个现象:

  1. 激活前敲 python 用的是系统解释器,激活后用的是环境解释器,能直接观察:Windows 上 where python、POSIX 上 which python 的输出会变;
  2. 退出环境用 deactivate,它把 PATH 恢复原状、清除 VIRTUAL_ENV;
  3. 不激活也能用虚拟环境:直接调用 .venv/Scripts/python.exe(Windows)或 .venv/bin/python(POSIX)就能运行环境里的 Python,脚本、CI 系统、VS Code(第 9 篇)都是这么干的。激活只是「省得敲长路径」的终端便利,不是隔离的必要条件。

为什么用标准库 venv 而不是 virtualenv

历史上有过第三方工具 virtualenv(功能更强,支持 Python 2 与 3 共存环境),但从 Python 3.3 起,标准库内置了 venv 模块,python -m venv 一行创建,无需安装任何东西,成为官方推荐;Unicode 与 pip 的行为在各版本间逐渐对齐。现代工程默认 python -m venv 即可;virtualenv 只在少数场景(如需要一个不装 pip 的极简环境、或显式指定 Python 版本)才有存在感。另外提醒:有些发行版(如 Debian/Ubuntu)不随 Python 自带 venv,需要 sudo apt install python3-venv——第 2 篇里我们列在安装命令中,原因就在这里。

隔离的边界:什么被隔离,什么没有

理解 venv 的能力边界,能避免「环境为什么还不干净」的困惑。被隔离的是第三方包(site-packages);没有被隔离的有三样:解释器本体(复用基础安装,见 pyvenv.cfg 的 home)、标准库(属于解释器的一部分,也复用)、环境变量与系统路径(你在终端 export 的变量照常可见)。换句话说:venv 只解决「依赖打架」,不解决「解释器版本不同」——后者的标准做法是装多个解释器版本(第 2 篇的多版本共存)而不是指望一个 venv 变出两个版本;想升级解释器(如 3.12 → 3.13)时,直接删掉 .venv 重新创建即可,五秒钟的事,不要尝试「迁移」环境。

对应地,也有一类场景不需要 venv:一次性系统脚本、容器镜像内的构建步骤,直接装系统级 Python 并 pip install 是合理的;但只要你并行维护两个以上项目,venv 就是默认姿势。

操作/实现

以下命令在三个平台通用(Windows 用 Scripts 目录、POSIX 用 bin 目录),括号内标注差异。请在项目的根目录执行,全程约两分钟。

第一步:创建环境。 在新目录(或项目根目录)执行:

python -m venv .venv

. 开头的目录名 .venv 是社区惯例(隐藏目录,避免与项目文件混在一起),可换任意名字(如 env)。创建后目录结构长这样(Windows 示例):

.venv/
├─ pyvenv.cfg        # 环境配置(上文已解读)
├─ Scripts/          # Windows:python.exe、activate 脚本、pip 等
│  ├─ activate
│  ├─ Activate.ps1
│  └─ python.exe
└─ Lib/site-packages # Windows:第三方包专用目录

(POSIX 下对应目录名为 bin/ 与 lib/python3.x/site-packages/。)

第二步:激活。 Windows 的 PowerShell / CMD:

.venv\Scripts\activate

POSIX(macOS / Linux 的 bash / zsh):

source .venv/bin/activate

激活成功后,终端提示符左侧通常会出现 (.venv) 前缀——这是环境给你的「我在环境里」的视觉信号。Git Bash 用户用 source .venv/Scripts/activate。

第三步:验证「真的换了解释器」。 本机(Windows + Git Bash)实测:

python -V
which python
python -c "import sys; print(sys.prefix)"

激活后的输出:

Python 3.12.10
/c/Users/<用户名>/AppData/Local/Temp/venv_demo/.venv/Scripts/python
C:\Users\<用户名>\AppData\Local\Temp\venv_demo\.venv

关键看第三行:sys.prefix 从系统解释器目录变成本项目 .venv 目录——这正是「当前环境变了」的硬证据(sys.prefix 是解释器安装根目录,第 3 篇我们用它确认过实现身份)。

第四步:在环境里装包。 激活状态下,pip 自动属于当前环境:

python -m pip install requests

本机实测(安装 requests 后立刻引用并打印版本):

requests 2.34.2 OK

注意 pip 与 python 现在完全一致——python -m pip install 装的就是 python 那个解释器的包,这正是第 2 篇易错点 2 推荐的写法的意义。想当场确认 pip 属于谁,执行 python -m pip --version,实测输出:

pip 25.0.1 from C:\Users\<用户名>\AppData\Local\Temp\venv_demo\.venv\Lib\site-packages\pip (python 3.12)

from ...\.venv\Lib\site-packages\pip 这一截就是证据:pip 本体也在这个环境里,与全局 pip 物理隔离。

第五步:退出并观察。

deactivate

退出后 which python 恢复指向系统解释器;sys.prefix 随之还原。删除环境最省心:直接删除 .venv 目录即可(venv 没有官方卸载命令,它只是一堆文件)。Windows 上若提示占用,先关闭使用了该解释器的终端窗口。

易错点与陷阱

易错点 1:装包时 pip 与 python 不是同一个解释器。 激活了环境却仍用全局的 pip(比如 macOS/Linux 上敲裸 pip、或 Windows 上装了多个 Python 的 pip.exe),包会装进全局而不是环境,然后出现「环境里 import requests 报 ModuleNotFoundError」的诡异情况。自查口令:激活后同时跑 which python 与 python -m pip --version,确保 pip 路径在 .venv 内。

易错点 2:Windows PowerShell 激活报「此系统上禁止运行脚本」。 现象是执行 Activate.ps1 被拒绝,这是 PowerShell 的执行策略(ExecutionPolicy)默认限制脚本所致,与 Python 无关。修复:只对当前用户放开本地脚本权限:

Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

RemoteSigned 的意思是:本地创建/修改的脚本可以运行,从网络下载的脚本仍须有签名——比完全放开(Unrestricted)安全得多。CMD 与 Git Bash 的 activate 脚本不受此限制。

易错点 3:把 .venv 目录当作「可以打包带走的东西」。 它包含绝对路径信息(pyvenv.cfg 里的 home、command),复制到别的目录或别的机器(尤其基础 Python 版本不同)会失效——所以不要把 .venv 提交进 Git、不要整体拷贝给同事。正确的「带走依赖」姿势是导出依赖清单(pip freeze,第 5 篇的正文),对方 python -m venv .venv 后一条命令装齐。

易错点 4:忘记自己激活着哪个环境。 终端提示符的 (.venv) 只在激活窗口可见;开了多个终端、切了目录,很容易在另一个全局环境里做操作。对策:每个项目固定用 .venv 名字,「先进目录、看提示符、再执行」三步走;脚本与 CI 一律用绝对路径调用 .venv/Scripts/python.exe,永不依赖激活状态。

小结

虚拟环境解决的是「依赖地狱」的第一个层次:每个项目拥有独立的 site-packages。它的原理很轻:不复制解释器,只做入口 + pyvenv.cfg 配置 + 专属包目录,靠修改 PATH 实现「激活」——理解了这一点,激活失败、sys.prefix 未变、PowerShell 报错三个现象都有了答案。实战部分我们走完了创建、激活、验证(which python 与 sys.prefix 双证据)、装包、退出、删除全流程。四个易错点覆盖了「pip 与 python 不一致」「PowerShell 执行策略」「venv 不可搬移」「环境混淆」。依赖隔离做到了,接下来该解决清单化:几百个包怎么记录、锁定、换源加速——这正是第 5 篇《包管理 pip 实战》的任务。

练习与思考题

  1. 在任意目录执行 python -m venv .venv,激活后运行 python -c "import sys; print(sys.prefix)",激活前后各打印一次,对比差异并解释。(答案导向:激活后输出 .venv 路径,说明解释器上下文已切换。)
  2. 阅读你机器上 .venv/pyvenv.cfg 的五个字段,说出 version 字段的作用。(答案导向:声明基础解释器版本,启动时校验次版本一致才允许运行。)
  3. 思考题:为什么 venv 不复制解释器本体,只复制入口?(答案导向:解释器与标准库都来自基础安装,复制本身浪费磁盘且升级不同步;隔离的真正对象是第三方包目录。)
  4. 场景题:你在全局环境装了 requests 2.34,又在项目 B 的 venv 里装了 requests 2.28,两个 import requests; requests.__version__ 分别输出什么?为什么互不影响?(答案导向:全局输出 2.34、项目 B 输出 2.28;两处 site-packages 物理独立,导入按各自解释器的路径搜索。)