"我这能跑,你那边装不上"——这类问题的现场通常长这样:
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. 自测
import的搜索路径里,哪一项决定 venv 里的包优先?venv隔离的是解释器还是site-packages?requests>=2.28,<3与requests==2.31.0各解决了什么问题、各留了什么风险?- 锁文件该由什么生成,为什么不该由
pip freeze生成? pip install -e .改了什么,使它适合开发期而不适合发布?- 为什么同一个包在同事机器上装得上,你这边却要编译?