在一台全新的 Linux 上敲 pip install requests,多数人并不清楚包装到了哪个目录、被哪个解释器看到。等 apt 开始报冲突、系统脚本突然 import 失败,才发现自己动的是发行版托管的那份 Python。虚拟环境不是"建议",它是唯一能让权限、版本、生命周期同时收敛的做法。
三个工具的分工一句话记住:venv 管隔离,pip 管解析与安装,uv 把前两者一起接管并补上锁文件。
1. 全局 site-packages 的灾难形态
$ which -a python python3 pip pip3
$ python3 -c "import sys, site; print(sys.prefix); print(site.getsitepackages())"
典型输出里 sys.prefix 是 /usr,site.getsitepackages() 指向 /usr/lib/python3.11/site-packages —— 这是系统解释器的目录,归属 apt/dnf 管理。
| 事故 | 触发方式 | 结果 |
|---|---|---|
| 系统工具崩溃 | pip 升级了被 apt 依赖的库(requests、urllib3) |
cloud-init、ansible 等脚本 import 报错 |
| 包管理器僵局 | pip 与 apt 装了同名包的不同版本 | apt upgrade 报文件冲突,或被静默覆盖 |
| 环境漂移 | 两台机器各自解析同一份松散约束 | 传递依赖版本不一致,行为不同 |
只要
sys.prefix是/usr或/usr/local,且这台机器是发行版托管 Python,任何pip install都该先停下来。
2. venv:创建、激活与各 shell 的差异
python3 -m venv .venv # 默认用当前 python3 的版本
python3.11 -m venv .venv # 指定解释器版本
python3 -m venv --upgrade-deps .venv # 顺手升级环境内的 pip/setuptools
激活命令因 shell 而异,这一步最容易卡住:
| Shell | 激活命令 | 说明 |
|---|---|---|
| bash / zsh | source .venv/bin/activate |
最常用 |
| fish | source .venv/bin/activate.fish |
venv 会单独生成 |
| csh / tcsh | source .venv/bin/activate.csh |
同上 |
| PowerShell | .venv\Scripts\Activate.ps1 |
Windows 路径 |
| cmd.exe | .venv\Scripts\activate.bat |
Windows |
| 不激活 | .venv/bin/python -m pip install X |
脚本/CI 里最稳 |
venv 目录结构解释了激活做了什么:pyvenv.cfg 记录基础解释器绝对路径,bin/activate 只是一段改 PATH、VIRTUAL_ENV、PS1 的 shell 脚本,bin/python 是指向基础解释器的符号链接而非拷贝。
激活只是改环境变量,不是切换解释器的运行机制。 CI 与 systemd 单元里不要依赖 source activate,直接写死解释器路径:
/opt/app/.venv/bin/python -m pip install -r requirements.txt
/opt/app/.venv/bin/python -m myapp
.venv/ 必须进 .gitignore;pyvenv.cfg 里是绝对路径,拷到别的机器上大概率失效。
3. pip 的解析器与 requirements.txt 的结构性缺陷
pip 从 20.3 起改用回溯解析器,能报出冲突而不是静默装错版本:
$ pip install -r requirements.txt
$ pip install "requests>=2.31,<3"
$ pip install -c constraints.txt -r requirements.in # 约束文件
$ pip check # 校验依赖自洽性
问题在于:requirements.txt 只表达"直接依赖的版本约束",不记录解析结果,同一个文件今天和三个月后可能装出两套依赖树。
| 文件类型 | 内容 | 能否复现 | 缺点 |
|---|---|---|---|
手写 requirements.txt |
只列直接依赖 | 否 | 传递依赖每次浮动 |
pip freeze 输出 |
全部依赖压平 | 是 | 分不清直接/间接,升级一个包要手工推断 |
| lock 文件(uv.lock / poetry.lock) | 全量解析 + 哈希 | 是 | 需配套工具 |
pip freeze 的误导性在于它"看起来像锁文件":能复现环境,但无法表达"只升级 fastapi 及其传递依赖"。
4. uv:更快,并且自带锁文件
uv 是 Rust 实现的包与环境管理器,把 venv、pip、pip-tools、pipx 的能力收在一个二进制里。它提供两条并行接口:
# A. 兼容模式:像 pip 一样用
uv venv --python 3.11 # 建环境
uv pip install -r requirements.txt # 装依赖,不写 pyproject
uv pip compile requirements.in -o requirements.txt # 等价 pip-tools 编译
uv pip sync requirements.txt # 让环境严格等于文件内容
# B. 项目模式:读 pyproject.toml,维护 uv.lock
uv init
uv add "httpx>=0.27" # 改 pyproject.toml + 更新 uv.lock + 同步环境
uv lock
uv sync # 让 .venv 严格等于 uv.lock
uv sync --frozen # 锁文件未变才执行,CI 用
uv run python script.py # 不必手动激活
| 维度 | uv pip ... |
uv sync |
|---|---|---|
读 pyproject.toml |
否(除非显式指定) | 是 |
| 维护锁文件 | 否 | 是(uv.lock) |
| 环境结果 | 由你给的文件决定 | 严格等于锁文件 |
| 会删多余包 | 否 | 会 |
| 适用场景 | 迁移老项目、CI 替换 pip | 新项目、团队协作 |
uv sync 会删除环境里不在锁文件中的包,这是它与 uv pip install 最大的行为差异。正是这个语义保证了可复现性。
5. pyproject.toml 与 requirements.txt 的取舍
PEP 621 把依赖写在 [project] 表(详见 S10-X04):
[project]
name = "demo-service"
version = "0.3.1"
requires-python = ">=3.11"
dependencies = ["httpx>=0.27", "pydantic>=2.6,<3"]
[project.optional-dependencies]
dev = ["pytest>=8", "ruff>=0.4"]
| 场景 | 建议 | 理由 |
|---|---|---|
| 应用/服务(自己部署) | pyproject.toml + 锁文件 |
直接依赖可读,锁文件可复现 |
| 库(给别人装) | 只用 pyproject.toml,不发锁文件 |
锁文件会与下游解析冲突 |
| 老项目仍用 requirements | 保留 + uv pip compile 生成编译版 |
迁移成本可控 |
requires-python 一定要写,它决定安装器在多版本环境下会不会挑到这个包。
6. pip install -e . 做了什么
-e(editable)让解释器从源码目录导入,而不是把文件复制进 site-packages。
| 方式 | 代码位置 | 改动即时生效 | 元数据 |
|---|---|---|---|
pip install . |
复制进 site-packages |
否 | 完整 |
pip install -e . |
指向源码目录 | 是 | 完整 |
sys.path.append() |
运行时改路径 | 是 | 无(无入口点、无依赖声明) |
现代实现走 PEP 660 的 editable wheel:构建后端在 site-packages 放一个 __editable__ 前缀的 .pth 或导入钩子,把包名映射回源码路径。所以后端必须支持 PEP 660(否则退回旧式 setup.py develop),且改了依赖或入口点后需要重跑一次。
7. PEP 668:externally-managed-environment
Ubuntu 23.04+ / Debian 12+ / Fedora 的系统 Python 带一个标记文件(/usr/lib/python3.11/EXTERNALLY-MANAGED),命中它时 pip 直接拒绝:
error: externally-managed-environment
× This environment is externally managed
╰─> To install Python packages system-wide, try apt install
python3-xyz, where xyz is the package you are trying to install.
(措辞随 pip 版本略有差异,按你机器上的 pip 版本核对。)原因很朴素:系统 Python 目录由包管理器拥有,pip 写入会破坏 apt 的一致性假设。三条正解:python3 -m venv .venv 后用环境内 pip 安装;pipx install <tool> 装全局 CLI 工具;docker run python:3.11 走独立解释器。
--break-system-packages 与 PIP_BREAK_SYSTEM_PACKAGES=1 只是关掉闸门,不解决任何问题。CI 里若确实需要系统级安装,换官方镜像或独立编译的解释器,而不是关闸。
8. 镜像源配置
| 位置 | 文件/变量 | 影响范围 |
|---|---|---|
| 用户级 pip | ~/.config/pip/pip.conf(macOS:~/Library/Application Support/pip/pip.conf) |
当前用户全部 pip |
| 环境级 pip | .venv/pip.conf |
仅该虚拟环境 |
| 全局 pip | /etc/pip.conf |
全机 |
| uv | UV_DEFAULT_INDEX / UV_INDEX_URL / uv.toml |
uv 全流程 |
| 单次 | -i / --extra-index-url |
一条命令 |
# 用户级 pip 配置 + CI 里的 uv 变量
export UV_DEFAULT_INDEX="https://pypi.org/simple"
易混点:--index-url 是替换默认源,--extra-index-url 是追加(同名包可能产生依赖混淆,私有源优先用前者);trusted-host 只在 HTTP 或自签证书时需要,配上等于关掉校验,不适合长期保留。
9. 排错:七种"装了却 import 不到"
| 现象 | 根因 | 处理 |
|---|---|---|
ModuleNotFoundError 但 pip list 里有 |
装到了另一个解释器 | pip -V 看路径,改用 .venv/bin/python -m pip |
| 激活后仍报找不到 | 没确认 which python |
进环境立刻查 which python 与 sys.prefix |
externally-managed-environment |
PEP 668 标记 | 建 venv,不要关闸 |
| editable 安装后改代码不生效 | 装的是非 editable 版本,或后端不支持 PEP 660 | 重装 -e .,确认 __editable__ 文件存在 |
uv sync 后少了一堆包 |
那些包不在 uv.lock 里 |
属预期;要保留就写进 dependencies |
激活之后一定要看 which python:conda、pyenv、asdf 的 shim 常排在 PATH 更前面,source activate 之后真正生效的解释器可能不是你预期的那个。
10. 边界:什么情况本就该用别的方案
| 场景 | 更合适的选择 | 理由 |
|---|---|---|
| 需要多 Python 版本切换 | pyenv / uv python install |
venv 不管理解释器版本 |
| 发布命令行工具给用户 | pipx / uv tool |
每个工具独立环境,不污染全局 |
| 生产部署隔离 | 容器镜像 + 锁文件 | venv 不隔离系统库与 glibc |
| 科学计算已有 conda 工作流 | conda / mamba |
非 Python 依赖(BLAS、CUDA)更强 |
| 需要审计依赖漏洞 | pip-audit + 锁文件哈希 |
venv 只管隔离 |
另一个常见误解:venv 不是安全沙箱,它只隔离包目录,系统库、环境变量、网络策略一概不隔离。
11. 自测
python -c "import sys; print(sys.prefix)"输出/usr,说明 pip 会装到哪里?风险是什么?- venv 激活本质上改了哪几个环境变量?为什么 CI 里更推荐直接写
.venv/bin/python? requirements.txt与 lock 文件的核心差别是什么?为什么pip freeze不能当锁文件长期维护?uv pip install与uv sync在"是否读 pyproject.toml、是否维护锁文件、是否会删包"三点上分别怎么表现?- 收到
externally-managed-environment报错,三条正解是什么?为什么--break-system-packages不算解法? pip install -e .在site-packages里留下什么文件来实现"代码改动即时生效"?