本章精髓 程序从来不是独自在跑。它依赖解释器、依赖库、依赖这台机器上的环境。打包就是把这些行李全装进一个盒子,让拿到它的人不必安装任何东西。这是"我写了一个脚本"与"我做了一个软件"之间的分界。
学完本章后,你应该能够:
- 说清打包做了什么,解释一个几十行的脚本为什么会打出十几兆的文件;
- 用打包工具把脚本打成单文件,并说明常用参数各自的作用与代价;
- 说清打包之后程序位置的变化,写出打包前后都能找到数据文件的代码;
- 为交付物写一份 README,说明用途、用法、数据位置与已知问题;
- 在一台没有安装过 Python 的计算机上,从双击开始验证自己的成品。
成品 · 装了什么进去 · 把风险写成问句
先看打包把什么装了进去,再看交付时到底要给几样东西。
1.1本章成品:一个双击就能用的程序
手上有一个借还登记工具,在自己电脑上跑得好好的。本章把它变成一个文件,拷进 U 盘,插到一台从来没装过 Python 的电脑上,双击就能用。
一行命令,若干分钟,得到一个十几兆的文件
1.2打包把什么装了进去
那个文件之所以能在没装过 Python 的电脑上运行,是因为它把运行所需的一切都带上了:
体积主要来自解释器与库,与你写了多少行几乎无关
1.3交付的不只是那个 exe
把一个 exe 单独发过去,对方多半用不起来。一份能被别人接着用下去的交付物有三样东西,缺一样都会在别人手里卡住。
打开 第14章演示_他机验收.html,把三样东西任意漏掉一样,看接收方在哪一步卡住。那台验收机上没有你,也没有你的解释。
1.4本章新技巧:把环境说全,把已知风险写成问句
问"怎么把 Python 打包成 exe",得到的是一段通用回答。通用回答的问题在于:打包的结果高度依赖你这台机器上的具体情况,系统版本、Python 版本、打包工具版本、用到了哪些库,任何一项不同,命令和坑都可能不一样。信息不全时,对方只能按最常见的情形作答,于是要来回三四轮才对得上。
所以提问时把环境一次性说全:系统、Python 版本、用到的库、要达到的效果。更进一步的做法是点名把已知的坑问出来,例如打包之后数据文件的位置会不会变。这类问题它通常不会主动提,问了才答。
1.5可直接复制的提示词
主提示词里的几条要求,分别对应后面要讲的知识点:
1.6大模型的常见偏差
给出的参数在你装的版本上不适用。 打包工具的参数改过几轮,它给的可能是另一个版本的写法。命令报错说"无法识别的参数"时,多半就是这个原因。把版本号一并告诉它,能省掉一轮来回。
不主动提路径会变这件事。 这是本章最需要防的一种。它给的代码在直接运行时完全正常,打包之后数据悄悄写进了临时目录。这个问题必须在提示词里点名问出来,否则它多半不会提。
建议安装一堆用不上的东西。 它有时会推荐额外的打包工具、虚拟环境管理器、图标转换软件。这些在正式项目里各有用处,本章一律不需要。装得越多,出问题的地方越多。
把数据文件也打进 exe 里。 它可能建议用参数把 items.db 一起打包进去。这样做之后数据变成只读的,登记的记录下次打开就没了。数据文件要放在 exe 旁边,不要打进去。
第14章演示_四个位置.html 把打包前后的四个位置并排摆出来,还能点着看数据文件落到哪里;第14章演示_他机验收.html 把验收机上的整个过程走一遍。第14章演示_登记器.py 已经写好正确的路径定位,可以直接用于对照阅读。依赖 · 环境 · 打包 · 参数
本章的核心内容集中在这一部分和下一部分。
这八个术语会原样出现在打包工具的文档、杀毒提示与开源项目的目录里。前四个在本部分讲解,后四个在第三部分讲解。
2.1四个知识点,一个一个看
上面一层要跑起来,下面两层缺一样都不行
现象:同一份 .py,在你机器上跑得好好的,拷到同学那儿提示找不到模块,或者干脆双击没反应。
概念:一个程序运行时用到的、但不是它自己写的那些东西,统称依赖(dependency);这些依赖加上系统本身的状况,合称环境。依赖是隐形的,你在自己机器上感觉不到它们,因为它们早就装好了。换一台机器,隐形的那部分才会现形。
类比:在家做菜不觉得自己带了多少东西,出门野炊才发现锅、火、盐一样都不能少。
Python 才能跑
不用装
差别不在代码,在于运行所需的东西带没带上
现象:把 exe 拷到一台没装过 Python 的电脑上,双击就能用。
概念:打包(bundling)是把你的代码、Python 解释器本身、以及用到的全部库,一起装进一个可执行文件。使用者的电脑上不需要有 Python,因为解释器就在这个文件里面。代价有两项:文件体积明显变大,以及打包过程本身对环境相当敏感,同样的命令在不同版本上结果可能不同。
类比:出门自带厨师和整套厨具,到哪儿都能开火,行李当然也就重。
某台机器上的实测值,换个系统或版本还会变
现象:一个几十行的脚本,打包出来十几兆。
概念:体积几乎与你写了多少行无关,取决于打包时把哪些东西装了进去。解释器本身就有若干兆,界面库、数据处理库各自还要占一块。用不到的库不要 import,是唯一有效的减小体积的办法。至于打出来到底多大,与系统、Python 版本、库的版本都有关系,不必追求某个具体数字,只要知道它由什么决定。
类比:行李箱的重量主要来自厨具,不是来自那张菜谱。
常用的就这四个,写法在不同版本上略有出入
现象:打包用的是一行命令,后面跟着几个以减号开头的参数。
概念:PyInstaller 是最常用的打包工具,用 pip 安装。它先分析你的代码用到了哪些库,再把这些库连同解释器一起收进去。参数的写法在不同版本上并不完全一致,从网上或大模型那里拿到命令之后,先用 pyinstaller --version 看一眼自己装的是哪个版本。
类比:打包行李时选箱型:要不要合并成一件、要不要贴自己的标签。
2.2打包命令:一行命令与四个参数
先用 pip 装上工具,再在脚本所在的文件夹里执行:
--onefile 打成单个文件,-n 指定输出的文件名。命令跑完之后会多出三样东西:dist 文件夹里是成品,build 文件夹与那个 .spec 文件是打包过程的中间产物,交付时不必给。
--noconsole,留着黑窗口,出了错还能看见报错;确认一切正常之后再加上它重打一次。这两次的先后顺序不能颠倒,先加参数再调试,等于把唯一的线索先关掉了。临时目录 · 控制台 · 误报 · 版本号
这一部分是本章唯一需要改代码的地方,也是最容易被忽略的一处。
3.1另外四个知识点
每次运行解压到不同目录,写进去的数据留不住
现象:直接运行时数据存在脚本旁边;打包之后同样的代码把数据写进了一个临时目录,而且每次运行的目录还不一样。
概念:单文件模式的程序运行时会先把自己解压到一个临时目录,__file__ 指向的是那里,程序结束后该目录随即被清理。要让数据文件留在可执行文件旁边,必须改用 sys.executable 来定位。这是本章最不容易发现的一处:程序不报错,数据却存不住。
类比:搬了家还按老地址寄东西。快递没有退回,只是送到了一个临时收件点,隔天就撤了。
先把错误接住,再动手隐藏控制台
现象:加上隐藏控制台的参数之后,程序出错时窗口连闪都不闪一下,什么都看不到。
概念:--noconsole 让双击时不弹出黑色控制台窗口,有界面的程序应当加。代价是原本打印在控制台里的报错也一并消失,程序出问题时使用者只看到窗口凭空关掉。因此加这个参数的前提是:程序里已经把可能出错的地方接住,并改用弹窗提示。
类比:把后台的对讲机关掉之前,前台得先有人应声,否则出了事外面一点动静都没有。
可能会损害你的设备。
拦的是"不认识",不是"有问题"
现象:打包好的文件被杀毒软件拦下,提示未知程序或可疑行为。
概念:打包出来的程序运行时会把自己解压到临时目录再执行,这种行为与部分恶意软件相似;加上文件没有代码签名(一份由第三方机构颁发、用于证明程序来源的凭证),因此常被误报。处理办法是把它加入信任列表,并预先写进 README。交给不熟悉的人时,这一条要写在最前面,否则对方大概率会直接删掉。
类比:新面孔进小区被门卫拦下。不是他做错了什么,是门卫不认识他。
版本号解决的是"你手上那份是哪一版"
现象:同一份程序发出去两次,对方报告一个问题,你不知道他用的是哪一版。
概念:版本号是给这一次发布出去的东西编个号,写在窗口标题、README 与文件名上,三处一致。发布指把这一版正式交出去,此后不再私自改动。对方报告问题时,你首先要确认的往往不是问题本身,而是他用的是不是最新版,版本号看上去只是一个数字,它解决的正是这件事。
类比:教材的第几版第几次印刷。不写清楚,两个人对着不同的书讨论同一页。
3.2四个位置,打包前后对照
下面这份脚本把程序运行时的四个位置全部打印出来,直接运行一次、打包之后再运行一次,差别一目了然。这是本章最值得亲手跑一遍的一个脚本。
# ① 是不是打包过的程序。打包之后 sys.frozen 才存在
frozen = getattr(sys, 'frozen', False)
# ② 源文件所在目录。打包之后指向临时解压目录,不是 exe 所在的地方
print('__file__ 所在目录 :', Path(__file__).parent)
# ③ 可执行文件所在目录。打包之后就是 exe 所在的那个文件夹
print('sys.executable 目录:', Path(sys.executable).parent)
# ④ 当前工作目录。取决于使用者从哪里启动,与程序放在哪里无关
print('当前工作目录 :', os.getcwd())
# ⑤ 打包时临时解压到哪里。只有打包后的单文件模式才有这一项
if hasattr(sys, '_MEIPASS'):
print('临时解压目录 :', sys._MEIPASS)
# ⑥ 本章要记住的写法:数据文件放在程序旁边时,用这个函数定位
def app_dir():
if getattr(sys, 'frozen', False):
return Path(sys.executable).parent
return Path(__file__).parent
把它打包一次,两次运行的结果对照如下。这是在同一台机器上实际跑出来的:
| 位置 | 直接运行 .py | 打包成单文件之后 |
|---|---|---|
__file__ 所在目录 | 脚本所在的文件夹 | 临时解压目录,每次运行都不同 |
sys.executable 目录 | Python 解释器所在目录 | exe 所在的文件夹 |
| 当前工作目录 | 启动命令时所在的位置 | 使用者从哪里启动就是哪里 |
sys._MEIPASS | 不存在 | 临时解压目录 |
/tmp/_MEI00000745dB0583,第二次写进了 /tmp/_MEI00000747bqcu0I,两次是不同的目录,而可执行文件旁边始终什么都没有。程序每次都提示写入成功,使用者却发现上次登记的记录不见了。这类问题在自己机器上直接运行 .py 时永远不会出现,只有打包之后才暴露。打开 第14章演示_四个位置.html,切换"直接运行"与"打包后",再点几次"写一条记录",看数据落在哪里、下次还在不在。
改路径 · 打包 · 换机验证
从一个只在自己机器上能跑的脚本,到一份别人拿到就能用的交付物。
4.1演示步骤
Path(__file__) 换成 app_dir()。改完先直接运行一次,确认功能没被改坏4.2前沿三分钟:把环境一起交付这件事
每章固定栏目 · 一条与本章内容相关的行业动态
"在我这儿能跑"是软件行业流传多年的一句话,指的正是本章这个问题:程序在开发者机器上正常,换一台就出状况,原因往往不在代码,而在两台机器的环境不同。行业为此发展出的解决思路始终是同一个方向,把程序运行所需的环境连同程序一起交付出去,让接收方不必自己配。
本章用的打包工具是这个思路在桌面程序上的做法。服务器端更常见的是容器技术,它把操作系统层面的依赖也一并封进一个镜像,接收方只要能运行容器就能跑起来。两者的做法不同,出发点完全一致:不要求对方复现你的环境,而是把环境带过去。
代价也是同一个:体积。带上整套环境之后,一个几十行的程序动辄十几兆,容器镜像上百兆更是常事。这笔交换在今天几乎总是划算的,因为磁盘比人的时间便宜得多,配环境这件事花掉的时间才是真正的成本。
三选一 · 六步 · 换台电脑验
上机环节由此开始,本次上机的成果就是作业 4,当堂完成并提交。
5.1三个题目,任选其一
把一个带数据库的登记工具打包成可执行文件。
通用把一个批量处理文件的工具打包,供不会用终端的人使用。
偏文科把一个多表合并统计的工具打包,交给需要出报表的人。
偏理工三个题目都要求成品能在一台没有装过 Python 的计算机上独立运行,并且要读写自己的数据文件。手上没有合适作品的,可以直接用演示件里的登记器作为起点。允许更换题材。
5.2上机六步
5.3共同验收标准
5.4常见错误与处理
pip install pyinstaller;仍不行时改用 python -m PyInstallerpyinstaller --version,把版本号连同报错一起发给大模型--hidden-import 显式指定即可5.5提交内容
提交方式:按教师提供的作业模板填写,文档开头注明所选题目。提交渠道见课堂通知,当堂提交。
5.6再进一步(选做)
把版本号写进程序:在窗口标题或界面上显示一个版本号,README 里也写同一个号,每改一次就往上加一位。
做完会碰到一个需要判断的问题:同一份程序发出去两次之后,你未必说得清对方手上是哪一版。对方报告一个问题时,你首先要确认的往往不是问题本身,而是他用的是不是最新版。版本号看上去只是一个数字,它解决的是这件事。
本章五条结论
- 程序从来不是独自在跑。打包就是把代码、解释器与全部依赖装进一个文件,体积主要来自后两样。
- 打包之后程序的位置变了。定位数据文件要用可执行文件所在目录,用错时程序不报错,数据却存不住。
- 两段式打包:先留黑窗口调通,再加隐藏参数重打。顺序颠倒,等于把唯一的线索先关掉。
- 交付物是三样:可执行文件、数据文件、说明文件。少一样,对方就会卡在某一步,而且多半不会来问。
- 自己机器上能跑不算数。必须在一台没有装过 Python 的计算机上,从双击开始完整走一遍。