2377 字
约 7 分钟
14
S10-B08 · Python 打包与依赖入门:venv、pyproject 与锁文件

"我这能跑,你那边装不上"——这类问题的现场通常长这样:

ImportError: cannot import name 'xxx' from 'pkg' (/usr/lib/python3/dist-packages/pkg/__init__.py)
ERROR: Could not find a version that satisfies the requirement pandas==2.1.0

第一条说明 import 到的是系统目录里的另一份,不是你以为的那份;第二条说明依赖约束在当前环境无解。两个症状对应两个机制:import 的搜索路径、依赖解析。下面把这两条线拆开,再给一个能复现的最小流程。


1. import 到底在哪些目录里找

$ python -c "import sys; print('\n'.join(sys.path))"
/home/yg/proj/.venv/lib/python3.11/site-packages   # 当前 venv
/usr/lib/python311.zip
/usr/lib/python3.11
/home/yg/.local/lib/python3.11/site-packages      # 用户级
/usr/local/lib/python3.11/dist-packages           # 系统级(Debian 系)
顺序 目录类型 谁写进去的
1 脚本所在目录 / 空串 启动方式决定(python -m 与 python x.py 不同)
2 site-packages(venv 内) 激活后由 pyvenv.cfg 决定
3 用户级 ~/.local pip install --user
4 系统级 dist-packages 发行版包管理器(apt)

"装了两份同一个库" 的成因就在这张表:venv 里一份、系统里一份,谁在前面谁生效。查实际加载的是哪份:

$ python -c "import pandas; print(pandas.__file__, pandas.__version__)"
$ pip -V      # 与 sys.prefix 指向不同目录,就说明装的和跑的不是一套

2. 虚拟环境隔离的到底是什么

方案 隔离对象 解释器 典型场景
venv site-packages(每环境一份) 仍是系统那个,靠符号链接 单项目、标准库够用
conda 整个前缀,含解释器与 C 库 独立二进制 需要非 Python 依赖(MKL、CUDA)
uv venv 同 venv,目录结构兼容 复用系统或自管 想要快的创建与安装

关键点:venv 不复制解释器,bin/python 只是链接。升级系统 Python 大版本后旧 venv 失效,原因是链接断了,不是环境坏了。

$ python -m venv .venv
$ source .venv/bin/activate        # Windows: .venv\Scripts\activate
$ python -c "import sys; print(sys.prefix)"   # 应指向 .venv
$ pip install -e ".[dev]"

3. requirements.txt 的三种写法

写法 例子 后果
裸名 requests 每次装当时最新,昨天能跑今天可能跑不了
精确固定 requests==2.31.0 可复现;但不含传递依赖,仍会漂移
范围 requests>=2.28,<3 兼顾更新与兼容;需锁文件兜底
附加:环境标记 pywin32==306; sys_platform == "win32" 跨平台必备
# 一个能用的 requirements.in(直接依赖,人工维护)
requests>=2.28,<3
fastapi~=0.110          # ~= 表示允许最后一位变化
uvicorn[standard]==0.29.*
pywin32==306; sys_platform == "win32"

4. pyproject.toml 的组成

[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "yg-tools"
version = "0.3.1"
requires-python = ">=3.10,<3.13"
dependencies = [
  "requests>=2.28,<3",
  "pydantic>=2.6",
]

[project.optional-dependencies]
dev = ["pytest>=8", "ruff>=0.4"]
web = ["fastapi>=0.110"]
段落 作用 不写会怎样
build-system 声明用哪个后端构建 老版本 pip 会退回 legacy setup.py
project.dependencies 直接依赖 装上后 import 失败,且没人提醒
project.requires-python 允许的解释器范围 在 3.8 上装成功但运行时语法错误
optional-dependencies 分组依赖(.[dev]) 只能靠额外 requirements 文件

pyproject.toml 是声明,setup.py 是脚本。有 build-system 时 setup.py 只是 setuptools 后端的配置补充,纯 pyproject.toml 项目可以没有它。两处都写 version 会打架。

5. 锁文件为什么必须存在

文件 内容 解决什么
requirements.in / pyproject 只写直接依赖与范围 表达"我要什么"
requirements.txt(锁定版) 全部依赖 + 精确版本 + hash 表达"上次装的是哪些"
uv.lock / poetry.lock 同上,含解析图与 hash 跨平台/跨 Python 版本可复现
# 方案 A:pip-tools
pip-compile requirements.in -o requirements.txt   # 生成锁
pip-sync requirements.txt                          # 按锁安装,多余的会卸掉

# 方案 B:uv
uv pip compile requirements.in -o requirements.txt
uv pip sync requirements.txt

pip freeze > requirements.txt 记录的是环境全部状态,连临时调试装的包也会进锁。锁文件该由直接依赖清单生成,而不是由环境反推。

6. pip install -e . 做了什么

$ pip install -e .
# 典型产物:.venv/lib/python3.11/site-packages/__editable__.yg_tools-0.3.1.pth

它做两件事:把项目注册进当前环境的包索引(于是 import yg_tools 可用),同时让源码目录成为实际加载位置(改代码不用重装)。适合开发期;发布或 CI 用 pip install . 装 wheel。

配套的包结构约定:

名字 是什么 判据
模块 单个 .py 文件 import utils
常规包 含 __init__.py 的目录 import yg_tools.core
命名空间包 无 __init__.py 的目录(PEP 420) 多个发行版共享同一顶层名

7. 直接依赖、间接依赖与冲突排法

$ pip check
pydantic 2.6.1 has requirement typing-extensions>=4.6.1, but you have typing-extensions 4.4.0.

$ pipdeptree --warn silence
yg-tools==0.3.1
├── pydantic==2.6.1
│   └── typing-extensions==4.4.0
└── requests==2.31.0
冲突形态 表现 排查手段
版本区间不相交 解析阶段就失败 ResolutionImpossible 看报错里的 The conflict is caused by:
已装版本不满足 装成功但 pip check 报警 pip check、pipdeptree -p 包名
同名包多来源 运行时报 import 错 看 __file__ 与 sys.path
二进制不兼容 导入时 undefined symbol 换平台 wheel

排法顺序:pip check 确认冲突 → pipdeptree -r -p 包名 找谁引入 → 再决定升级还是加约束覆盖。直接 pip install --upgrade 一通,往往把别的依赖弄坏。

8. 私有源与镜像的优先级

优先级 来源 写法
1(最高) 命令行参数 pip install -i URL --extra-index-url URL2
2 环境变量 PIP_INDEX_URL、PIP_EXTRA_INDEX_URL
3 虚拟环境内 pip.conf .venv/pip.conf
4 用户级 ~/.config/pip/pip.conf(Linux)
5(最低) 全局 /etc/pip.conf
# ~/.config/pip/pip.conf
[global]
index-url = https://mirror.example.com/simple
extra-index-url = https://pypi.org/simple
trusted-host = mirror.example.com
timeout = 30

高频坑:index-url 指私有源、extra-index-url 指公网时,pip 两边都查并取版本最高者——内部包可能被同名公网包顶掉。内部包名建议加前缀。

9. wheel 与 sdist

形态 后缀 内容 安装时是否需要构建
wheel .whl 预构建的二进制/纯 Python 包 否,解压即装
sdist .tar.gz 源码 + 构建脚本 是,需要编译器与构建依赖

"有源码为什么还编译失败":pip 下不到匹配平台的 wheel 时会退回 sdist 就地构建,于是需要 gcc、python3-dev、对应 C 库。平台标签不通用是常态:

numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.whl   # Linux x86_64 + CPython 3.11
numpy-1.26.4-cp311-cp311-win_amd64.whl               # Windows,Linux 上装不了

旧版本 Python 或新架构(如 arm64)常缺 wheel,于是"同事能装、你装不上"——差别在平台标签,不在代码。

10. 最小示例:从零到可复现

# ① 建环境(不要在系统 Python 里装)
python -m venv .venv && source .venv/bin/activate
python -V                              # 记下版本,写进 requires-python

# ② 写声明
cat > pyproject.toml <<'EOF'
[project]
name = "yg-tools"
version = "0.1.0"
requires-python = ">=3.10,<3.13"
dependencies = ["requests>=2.28,<3"]
EOF

# ③ 以可编辑方式装上,便于开发
pip install -e .

# ④ 生成锁文件(锁里含传递依赖)
pip install pip-tools
pip-compile pyproject.toml -o requirements.txt

# ⑤ 在干净环境复现
deactivate && rm -rf .venv && python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt && pip check

第 ⑤ 步是验收:换一个空环境,仅凭锁文件能否装出同样的东西。跑不通说明锁与声明不一致。

11. 排错

现象 原因 处理
ImportError 指向 dist-packages 未激活 venv,或系统里也装了一份 激活 venv,print(sys.path) 核对首位
Could not find a version that satisfies 源里没有该版本 / 平台标签不匹配 查 pip index versions 包名,或放宽范围
ResolutionImpossible 两个直接依赖的区间不相交 看 The conflict is caused by: 逐条放宽
undefined symbol wheel 与本地 C 库版本不匹配 换 wheel 版本或从源码构建
pip install 报权限错 用了系统 Python 建 venv;不要 sudo pip
锁文件与 pyproject 不一致 改了声明没重生成锁 CI 加一步 pip-compile --dry-run

12. 边界

情况 更合适的做法
需要精确复现生产依赖 锁文件 + 容器镜像,而不是"同一个 venv"
多项目共用同一组包 各自 venv,接受磁盘开销
需要非 Python 的运行时依赖 conda / 系统包 / 容器
发布给别人用 构建 wheel 上传,别让人从源码装

一句话:venv 管隔离,pyproject 管声明,锁文件管复现,三件事不能互相替代。

13. 自测

  1. import 的搜索路径里,哪一项决定 venv 里的包优先?
  2. venv 隔离的是解释器还是 site-packages?
  3. requests>=2.28,<3 与 requests==2.31.0 各解决了什么问题、各留了什么风险?
  4. 锁文件该由什么生成,为什么不该由 pip freeze 生成?
  5. pip install -e . 改了什么,使它适合开发期而不适合发布?
  6. 为什么同一个包在同事机器上装得上,你这边却要编译?
S10-B08 · Python 打包与依赖入门:venv、pyproject 与锁文件
https://www.clxhxhhr.top/posts/6138/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。