第4章 PyInstaller进阶:多进程/多线程支持、C扩展模块处理、虚拟环境打包、Docker化打包
说实话,很多同学用PyInstaller打包普通脚本已经没问题了。但一遇到多进程、C扩展、虚拟环境这些场景,就开始抓瞎。我当年也是踩了不少坑才摸清楚门道。今天咱们就把这些进阶玩法一次性讲透。
4.1 多进程/多线程支持
先说说多进程。PyInstaller打包多进程程序时,有个经典问题——子进程启动失败。为什么会这样?因为打包后,Python解释器被替换成了你的exe文件。子进程默认会尝试启动python.exe,但打包环境里根本没有这玩意儿。
解决方案其实很简单。我习惯用 multiprocessing.freeze_support() 这个函数。看代码:
# main.py
import multiprocessing
import os
def worker(name):
print(f"子进程 {name} 正在运行,PID: {os.getpid()}")
if __name__ == '__main__':
multiprocessing.freeze_support() # 这一行是关键
processes = []
for i in range(4):
p = multiprocessing.Process(target=worker, args=(i,))
processes.append(p)
p.start()
for p in processes:
p.join()
打包命令和普通脚本一样:
pyinstaller --onefile main.py
多线程就简单多了。Python的GIL虽然限制了并行,但I/O密集型任务用多线程完全没问题。PyInstaller对 threading 模块支持得很好,基本不需要额外配置。
concurrent.futures.ProcessPoolExecutor 打包后报错。后来发现需要在主模块里加上 if __name__ == '__main__' 保护。你想想看,这其实和普通Python脚本的写法是一样的,只是打包后更容易暴露问题。
4.2 C扩展模块处理
C扩展模块,说白了就是那些用C/C++写的Python库。比如 numpy、pandas、opencv-python 这些。PyInstaller处理它们时,有时候会漏掉一些动态链接库(.dll或.so文件)。
我建议你打包前先做个检查。用 --hidden-import 参数手动指定可能遗漏的模块:
pyinstaller --onefile --hidden-import=numpy.core._multiarray_umath app.py
但一个个手动加太累了。更聪明的做法是用 --collect-all 参数:
pyinstaller --onefile --collect-all=numpy app.py
这个参数会收集指定包的所有数据文件和动态库。嗯,这里要注意——它会增加打包体积。我有个项目用了 --collect-all=opencv-python,打包后从30MB直接飙到150MB。
| 常见C扩展库 | 推荐处理方式 | 注意事项 |
|---|---|---|
| numpy | --collect-all=numpy | 体积增加约20MB |
| opencv-python | --collect-all=cv2 | 体积增加约100MB |
| pandas | --hidden-import=pandas._libs.* | 注意通配符用法 |
| lxml | --collect-all=lxml | 依赖libxml2和libxslt |
--add-binary 或 --collect-all 补上。别一上来就全量收集,那太粗暴了。
4.3 虚拟环境打包
虚拟环境打包,说白了就是让打包环境干净一点。我见过太多人直接在系统Python环境里打包,结果把一堆用不到的库也打包进去了。最后exe文件几百MB,运行还各种报错。
正确的做法是这样的:
- 创建一个新的虚拟环境
- 只安装项目需要的依赖
- 在这个环境里执行PyInstaller
# 创建虚拟环境
python -m venv myenv
# 激活虚拟环境(Windows)
myenv\Scripts\activate
# 激活虚拟环境(macOS/Linux)
source myenv/bin/activate
# 只安装必要的依赖
pip install pyinstaller
pip install requests # 你的项目依赖
# 打包
pyinstaller --onefile app.py
我个人习惯用 requirements.txt 来管理依赖。这样别人拿到项目也能复现同样的打包环境:
pip install -r requirements.txt
pyinstaller --onefile app.py
4.4 Docker化打包
Docker化打包,是解决环境不一致问题的终极方案。说白了,就是把打包环境做成一个镜像,谁用都一样。
我给你们看一个我常用的Dockerfile:
# Dockerfile
FROM python:3.10-slim
# 安装系统依赖
RUN apt-get update && apt-get install -y \
gcc \
libc6-dev \
&& rm -rf /var/lib/apt/lists/*
# 安装PyInstaller
RUN pip install pyinstaller
# 设置工作目录
WORKDIR /app
# 复制项目文件
COPY . .
# 打包
RUN pyinstaller --onefile --name myapp app.py
# 输出打包结果
CMD ["cp", "/app/dist/myapp", "/output/"]
构建和运行:
# 构建镜像
docker build -t pyinstaller-builder .
# 运行容器,获取打包结果
docker run --rm -v ${PWD}/output:/output pyinstaller-builder
这样做的好处很明显:
- 环境一致:不管你在Windows、macOS还是Linux上跑,打包环境都一样
- 可重复:同一个Dockerfile,任何时候打包结果都一样
- 干净:不会污染你的系统Python环境
python:3.10-slim 而不是 python:3.10。slim版本体积小很多,而且打包时不需要那些开发工具。如果你需要编译C扩展,再考虑用完整版。
还有一个技巧——用 docker-compose 来管理多平台打包:
# docker-compose.yml
version: '3'
services:
builder-linux:
build:
context: .
dockerfile: Dockerfile.linux
volumes:
- ./output:/output
builder-windows:
build:
context: .
dockerfile: Dockerfile.windows
volumes:
- ./output:/output
这样一条命令就能同时打出Linux和Windows的包:
docker-compose up builder-linux builder-windows
freeze_support(),C扩展用 --collect-all 或 --hidden-import,虚拟环境保证环境干净,Docker化打包解决环境一致性问题。这四个进阶技巧,基本能覆盖90%的复杂打包场景。
好了,这一章的内容就到这里。下一章咱们聊聊安装器制作——怎么把你的exe包装成一个漂亮的安装程序,让用户一键安装。