Python 项目换台机器就跑不起来:从虚拟环境到系统依赖的排查顺序
先别改代码,确认解释器是谁
“在我电脑上是好的”通常不是一句玩笑,而是环境不一致的真实写照。第一步不是重装依赖,而是确认当前命令到底用了哪个 Python:
which -a python python3
python -V
python -c "import sys; print(sys.executable); print(sys.version)"
which -a 会把 PATH 里所有同名命令都列出来。常见情况是 /usr/bin/python3 和 /usr/local/bin/python3 同时存在,python 指向其中一个,pip 却指向另一个。用 pip -V 看它对应的 site-packages 路径,如果和 sys.executable 不在同一个前缀下,后面装什么都会错位。
虚拟环境有没有真正激活
激活虚拟环境本质上只是修改 PATH 和几个环境变量,它不会跟着代码走。IDE 的运行配置、systemd 服务、cron 任务、Dockerfile 里的 RUN,都可能在一个“没有激活”的 shell 里执行。先看三个值:
echo "$VIRTUAL_ENV"
python -c "import sys; print(sys.prefix)"
pip -V
正常激活时,sys.prefix 应该指向项目下的 .venv,而不是 /usr。在服务和定时任务里,不要依赖 source activate,直接写虚拟环境解释器的绝对路径更稳:
/opt/app/.venv/bin/python /opt/app/main.py
Windows 下对应 .venv\Scripts\python.exe。如果项目用 Poetry、PDM 或 uv,优先用 poetry run、pdm run、uv run,它们会自动找到项目环境,不依赖当前 shell 是否激活。
依赖是不是按锁文件装的
只写 pip install -r requirements.txt 还不够,关键是 requirements 里有没有固定版本和传递依赖。开发机上可能早就装过某个包,pip install 直接跳过了;新机器从零开始,解析出来的版本就可能不同。先做一次一致性检查:
pip check
pip list --outdated
python -m pip debug --verbose | head -n 30
pip check 会报告依赖冲突;pip debug 能看到当前解释器支持的 wheel 标签。团队项目建议提交 poetry.lock、pdm.lock 或 uv.lock,安装时用 frozen 模式,例如 uv sync --frozen。只有 requirements.txt 时,至少把直接依赖和关键传递依赖都固定版本,并区分开发依赖与运行依赖。
另一个容易忽略的点是平台标记。某些包只在 Windows 安装,某些包只有 Linux 轮子:
pywin32==306; sys_platform == "win32"
uvloop==0.21.0; sys_platform != "win32"
如果开发机是 macOS、服务器是 Linux,这类差异必须提前处理,否则锁文件看起来一致,装出来的东西却完全不同。
ModuleNotFoundError 和 ImportError 不是一回事
前者通常是包没装或装到了别的解释器;后者更可能是包在,但内部的二进制扩展加载失败。先用一条命令确认模块的真实位置和版本:
python -c "import numpy; print(numpy.__file__, numpy.__version__)"
如果报 undefined symbol、GLIBC not found 或 cannot open shared object file,问题就在编译扩展和系统库,而不是 pip 源。可以继续查依赖:
ldd $(python -c "import numpy, os; print(os.path.dirname(numpy.__file__))")/_multiarray_umath*.so | grep "not found"
这类错误常见于跨发行版复制虚拟环境、在 Alpine 上装 glibc 轮子、或者升级 Python 小版本后继续用旧环境的 .so。虚拟环境不要跨机器打包复制,应该在目标机器上重建。
系统库和编译工具补齐了吗
很多 Python 包只是系统库的封装。服务器是最小化镜像时,pip 找不到预编译轮子,就会退化成源码编译,然后因为缺头文件失败:
psycopg2需要libpq-dev和pg_config;mysqlclient需要 MySQL 客户端开发库;Pillow需要 libjpeg、zlib;cryptography需要 OpenSSL 开发库;lxml需要 libxml2、libxslt。
先看能不能只装预编译轮子:
python -m pip install --only-binary=:all: -r requirements.txt
如果这条命令失败,说明某个包没有当前平台的 wheel,需要补系统包或编译工具。不要在生产镜像里长期保留 build-essential,可以分阶段构建,把编译产物复制到运行镜像。
环境变量和配置文件也要对比
代码能导入、依赖也齐了,仍然可能启动失败,因为配置没有跟着过去。.env 是否被加载、变量名大小写是否一致、systemd 有没有写 EnvironmentFile、Docker 有没有传 --env-file,这些都要逐项确认。排查时只打印键名,不要把密钥值打到日志里:
env | cut -d= -f1 | sort
python -c "import os; print(sorted(k for k in os.environ if k.isupper()))"
一套可复用的排查顺序
- 确认
python、pip指向同一个解释器; - 确认
sys.prefix是项目虚拟环境,而不是系统 Python; - 按锁文件重装依赖,跑一次
pip check; - 区分模块缺失和二进制扩展加载失败;
- 补齐系统库和编译工具,优先使用预编译 wheel;
- 对比环境变量和配置文件,确认服务进程也能读到。
最后,把可运行环境固化成 Dockerfile 或 devcontainer,至少把 Python 版本、锁文件和系统包写进仓库。环境这件事,文档写十遍不如一条能重复执行的构建命令。
小结
换机器跑不起来,绝大多数时候不是代码写错了,而是解释器、虚拟环境、依赖版本、系统库这四层里有一层没对齐。按顺序排查,比一上来就删环境重装更快,也更不容易把问题越修越乱。
- 点赞
- 收藏
- 关注作者
评论(0)