2、修改文件打开函数
这里有三种方法,前两种是我的方法,最后一种是网上别人的方法。
① 引入特定的模块
这个模块的代码很简单,放在下面,一定要将模块命名为 _.py,并在引用其他第三方模块之前就引用它,但又一定要在下划线开头的模块之后引用(否则会有 BUG),它在主 Python 文件里引用一次即可(其他的文件不用引用)!它可以将 open 函数改成我们想要的,而且原来的代码还完全不用修改!
import builtins def wrapper(function): def _open(*args, **kw): """ 修改路径 """ _args = list(args) _args[0] = __file__[:-4] + args[0] if kw.get('file'): kw['file'] = __file__[:-4] + kw['file'] return function(*_args, **kw) return _open setattr(builtins, 'open', wrapper(open))
这里的 __file__ 是 Python 文件的属性,是一个字符串,为该文件的绝对路径,不管该文件在哪里,__file__ 都是对应的绝对路径。然后我们用写一个 wrapper 函数充当装饰器,将内置的 open 函数包装一下。再引入 builtins 模块(内置函数和类的模块),给其添加一个名为 open 新属性以覆盖原来的 open 函数,并对该项目整体生效即可!
这种方法的好处在于,它只需要在主文件里引用一次即可,其他的什么都不用改!(④ 特别注意 里的除外)
② 自己手动修改 open 函数修改路径
这个修改是在源代码中修改的(每一个用到了 open 函数的 Python 文件都要改一次),目的就是要让相对路径变成会根据主 Python 文件的路径而变化的绝对路径。修改的装饰器如下:
# 编写装饰器 def wrapper(function): def _open(*args, **kw): """ 修改路径 """ args_list = list(args) key = '/'.join(__file__.split('\\')[:-1]) + '/' args_list[0] = key + args[0] if kw.get('file'): kw['file'] = key + kw['file'] return function(*args_list, **kw) return _open # 装饰内置函数open open = wrapper(open)
把这段代码写在文件的开头即可(或者说在使用open函数之前)。
③ 网友的其他方法
他们就是写了这样一个函数来代替 open,也是手动修改的 open 函数,不得不说,看起来有点麻烦(每个用了 open 函数的 Python 文件都要引入 os 和 sys 模块)。
import os import sys def get_resource_path(relative_path): if hasattr(sys, '_MEIPASS'): return os.path.join(sys._MEIPASS, relative_path) return os.path.join(os.path.abspath("."), relative_path)
其他的都是一样的。
④ 特别注意
这里还要提一下,无论是前面的哪一种方法,只要你使用了参数为路径的其他函数时,也要改一下,其实就是在相对路径前面加上方法②中的 key 即可。
其实直接方法③来代替也可以,但是功能上容易出错,而且如果 Python 文件较多,那么每个 Python 文件都这样引用两个模块(sys 和 os),看起来比较麻烦。
当我们使用了 tkinter 模块的时候,PhotoImage 类就是要这样写的一个例子(其中的 __init__ 方法用到了路径):
class PhotoImage(tkinter.PhotoImage): def __init__(self, *args, **kw): if kw.get('file'): key = '/'.join(__file__.split('\\')[:-1]) + '/' kw['file'] = key + kw['file'] tkinter.PhotoImage.__init__(self, *args, **kw)
这个代码就要写在使用 PhotoImage 的开头,后续调用时就用这个 PhotoImage,使用其他模块时,遇到参数为路径的函数或类,都要这样修改。
最后一步,和之前的方法一样,打包你的程序即可!
打包实战
我这里以一个我的半成品为例,进行打包。项目是一个图形化界面的程序。我们要将其打包成只含有一个 exe 的文件。
打包方式的选择
我的项目里面包含多个 Python 文件,要用多 Python 文件打包方式;
项目比较大,为节省打包时间,并追求极致的 exe 大小,采用虚拟环境打包方式;
项目含有资源文件夹,采用包含资源文件的打包方式。
打包方式选择好了,开始打包!
打包全过程
项目全部文件(蓝色背景的是主文件)项目文件
包含四个 Python 文件、一个资源文件夹(res),资源文件夹里面又包含了一些子文件夹和 json 文件。
第一步:启动 Anaconda Prompt,切换至目标文件夹路径位置
cd C:\Users\小康\Desktop\SuperGameLauncher
第二步:启动虚拟环境(我的是一个纯净的、第三方包只有 Pyinstaller 的环境)
conda activate e1
启动虚拟环境切换至目标文件夹路径位置
第二步:启动虚拟环境(我的是一个纯净的、第三方包只有 Pyinstaller 的环境)
conda activate e1
启动虚拟环境
第四步:引入 _.py 模块(我的程序用到了大量 open 函数且涉及多文件)
_.py 自定义模块
引入 _ 模块
处理特殊的情况(tkintertools 模块里面有参数为路径的类):
处理特殊情况
第五步:编辑 spec 文件
修改图中标识的两处地方(_.py 不要忘记了)。
编辑 spec 文件
第六步:打包项目(注意这里的对象是 spec 文件)
Pyinstaller SuperGameLauncher.spec
准备打包
打包成功!
打包成功
第七步:检验打包效果
找到项目中的 dist 文件夹,打开后会有一个 exe 文件。对于我这个项目而言,这已经是非常小的大小了(50.8MB),毕竟资源文件就有 42.2MB,也就是说,除去资源文件,源代码占用的大小为 8.6MB!怎么样,是不是很不错呢?
dist 文件夹
双击运行!
成功运行