2527 字
约 8 分钟
17
S10-B06 · Python 虚拟环境与依赖:venv、pip、uv 的分工

在一台全新的 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. 自测

  1. python -c "import sys; print(sys.prefix)" 输出 /usr,说明 pip 会装到哪里?风险是什么?
  2. venv 激活本质上改了哪几个环境变量?为什么 CI 里更推荐直接写 .venv/bin/python?
  3. requirements.txt 与 lock 文件的核心差别是什么?为什么 pip freeze 不能当锁文件长期维护?
  4. uv pip install 与 uv sync 在"是否读 pyproject.toml、是否维护锁文件、是否会删包"三点上分别怎么表现?
  5. 收到 externally-managed-environment 报错,三条正解是什么?为什么 --break-system-packages 不算解法?
  6. pip install -e . 在 site-packages 里留下什么文件来实现"代码改动即时生效"?
S10-B06 · Python 虚拟环境与依赖:venv、pip、uv 的分工
https://www.clxhxhhr.top/posts/5742/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。