Python 虚拟环境:venv 与 poetry 的取舍
Python 虚拟环境:venv 与 poetry 的取舍
“我明明激活了虚拟环境,pip 装完还是装到全局去了”——这句话我在不同团队里听过至少二十次。虚拟环境本身不复杂,但它牵扯到 PATH、sys.prefix、解释器查找顺序、IDE 配置四件事,任何一处没对齐,就会出现"看起来激活了、实际没生效"的假象。
这篇不站队 venv 或 poetry,只讲清楚两者各自解决了什么问题、代价是什么,以及在什么规模下该切。
一、venv 到底做了什么
很多人以为 venv 是"隔离环境",这个说法容易让人以为有沙箱或命名空间。实际上它只做三件事:
- 建一个目录,里面放一个指向基础解释器的
python(Windows 是拷贝或 shim) - 写一份
pyvenv.cfg,记录基础解释器的位置 - 生成
activate脚本,作用是把该目录的bin(Windows 是Scripts)插到 PATH 最前面
$ python -m venv .venv
$ cat .venv/pyvenv.cfg
home = /usr/bin
include-system-site-packages = false
version = 3.11.9
隔离的全部机制就是这个 home 字段:解释器启动时读 pyvenv.cfg,据此把 sys.prefix 从 /usr 改成 .venv,于是 site-packages 的搜索路径也跟着变。include-system-site-packages = false 则关掉了用户级 site-packages,所以 venv 里看不到 ~/.local/lib 下那堆东西。
没有任何沙箱。 所以 venv 里的进程照样能读全局文件、能装编译依赖、能污染 ~/。理解这一点,后面的坑就好解释了。
二、四个"激活了但没生效"的坑
坑 1:pip install 装到了全局
先看现象:
$ source .venv/bin/activate
(.venv) $ pip install requests
Requirement already satisfied: requests in /usr/lib/python3/dist-packages
pip 说"已满足",路径却是 /usr/lib。这说明跑的不是 venv 里的 pip。
排查只有一步——确认实际执行的是谁:
(.venv) $ which -a pip python
/home/me/proj/.venv/bin/pip
/usr/bin/pip
/home/me/proj/.venv/bin/python
/usr/bin/python
which -a 会把 PATH 上所有同名命令都列出来,第一行才是真正会执行的。如果第一行不是 .venv/bin 下的,说明 activate 没生效(常见于在子 shell 里 source、或用 sh 而不是 bash 执行脚本)。
坑 2:pip 和 python -m pip 不是一回事
即使 PATH 对了,pip 这个可执行文件也可能属于另一个解释器。系统里同时装了 pyenv、conda、apt 的 python 时特别容易乱。
可靠写法是永远用模块方式调用:
$ python -m pip install requests
python -m pip 的含义是"用当前这个 python 去加载 pip 模块",pip 和解释器在构造上就是同一个,不可能错配。这一条建议写进团队规范,成本极低,收益极高。
顺手加一道保险,让 pip 在虚拟环境外直接拒绝工作:
$ export PIP_REQUIRE_VIRTUALENV=1
$ pip install requests
ERROR: Could not find an activated virtual environment
这条可以写进 ~/.bashrc,能挡住大部分手滑。
坑 3:IDE 没跟着换解释器
命令行里一切正常,一开 IDE 补全全红、运行报 ModuleNotFoundError——IDE 有自己独立的解释器设置,跟终端里的 activate 毫无关系。
VS Code 用 Ctrl+Shift+P → Python: Select Interpreter,选到 .venv/bin/python;PyCharm 在 Settings → Project → Python Interpreter。选完记得看一眼工作区设置 .vscode/settings.json 里是否有:
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python"
}
提交这个文件能让整个团队少踩一次。
坑 4:把 .venv 提交进了 Git
.venv 里有绝对路径(pyvenv.cfg 的 home、activate 里的 VIRTUAL_ENV),换台机器或换个目录就直接坏掉,而且体积巨大。
$ echo ".venv/" >> .gitignore
如果已经提交过,光加 .gitignore 不够,历史里还在。需要 git rm -r --cached .venv 再提交一次。
三、poetry 多解决了什么
venv 只负责"隔离运行时",不负责"管理依赖"。依赖管理的痛点在于传递依赖:你写了 requests==2.31.0,它下面还挂着 urllib3、certifi、idna、charset-normalizer,pip install 装的时候版本由 pip 当场解析,今天装和三个月后装可能得到不同结果。
poetry 相对 venv 多的三件事:
- 有约束求解器:
pyproject.toml里写的是版本区间,poetry 求解出一组互相兼容的具体版本 - 有锁文件:
poetry.lock记录每个包的确切版本 + 哈希,保证换机器装出完全一致的依赖树 - 区分直接依赖与传递依赖:主依赖写在
[tool.poetry.dependencies],锁文件里两者都记
代价也很实在:多一层工具、多一份配置、多一个学习成本,以及导出标准依赖清单需要额外一步。poetry 1.x 内置 poetry export,2.x 把它拆成了插件,得先装:
$ poetry self add poetry-plugin-export
$ poetry export -f requirements.txt --output requirements.txt --without-hashes
四、怎么选:看协作规模
判断标准其实只有一条——是不是多人协作、是否需要可复现的构建。
| 场景 | 建议 | 理由 |
|---|---|---|
| 一次性脚本、临时验证 | venv | 不需要锁文件,pip install 够用 |
| 单人小工具 | venv + requirements.txt(全部固定版本) |
成本最低,够用 |
| 团队项目 | poetry | 锁文件能避免"我这能跑" |
| 要发布到 PyPI | poetry | build + publish 一条链 |
| 需要打进 Docker 镜像 | 见下节 | 分层与锁文件的关系要单独处理 |
有一件事要强调:requirements.txt 不等于锁文件。手写的 requirements.txt 通常只固定直接依赖,传递依赖仍是浮动的;pip freeze 导出的版本虽然把传递依赖也固定了,但没有哈希校验,也没有"为什么锁在这个版本"的记录。如果团队只有 requirements.txt,最低要求是必须用 pip freeze 生成、必须提交、必须固定所有版本。
五、我实际在用的混合做法
生产项目上我一般这么配,兼顾两边的优点:
1) 让 poetry 把虚拟环境建在项目目录里
$ poetry config virtualenvs.in-project true
默认情况下 poetry 把 venv 建在 ~/.cache/pypoetry/virtualenvs/ 下,名字是一串哈希,找起来痛苦。改成 true 后会在项目里建 .venv,IDE 能自动发现,rm -rf .venv 就是干净重置。
2) 生产安装只认锁文件,不装开发依赖
$ poetry install --only main --no-root
--only main 跳过 dev 组,--no-root 不把项目自身装成包(镜像里不需要)。
3) Docker 里用两段式安装,避免每次改代码都重装依赖
FROM python:3.11-slim
WORKDIR /app
RUN pip install --no-cache-dir poetry==1.8.3
COPY pyproject.toml poetry.lock ./
RUN poetry config virtualenvs.create false \
&& poetry install --only main --no-root
COPY . .
CMD ["python", "-m", "app"]
关键在 virtualenvs.create false——容器本身就是隔离环境,再套一层 venv 纯属浪费。以及 COPY 顺序:先只拷两个依赖描述文件、装完再拷代码,这样改业务代码时这一层镜像能命中缓存。
六、一张自检清单
接手一个新项目时,我会按顺序跑这几条,基本能在两分钟内判断环境是否健康:
$ which -a python pip # 第一行是否指向 .venv
$ python -c "import sys; print(sys.prefix)" # 是否等于项目内 .venv 的绝对路径
$ python -m pip list --outdated | head # 能否正常读到当前环境的包
$ git ls-files | grep -E "^\.venv/" # 应无输出
四条全过,环境基本没问题。
至于工具本身,venv + 固定版本的 requirements.txt 能覆盖八成场景,剩下的两成交给 poetry。真正决定项目能不能复现的,不是选了哪个工具,而是锁文件有没有提交、装依赖时有没有绕过它。 换工具能解决的问题是有限的,纪律问题换不掉。
顺带一提,如果你已经觉得 poetry 太重,可以看看 uv——同样是锁文件模型,速度是 poetry 的几十倍,接口也兼容 requirements.txt。但要换工具,最好等手上项目结项时再动,别在交付期折腾环境。
- 点赞
- 收藏
- 关注作者
评论(0)