目录
背景
为了将Python程序发布到没有Python环境的客户端PC上运行,就需要将Python程序连同Python环境一同打包发布。
Python环境就是指包含了特定版本Python解析器以及一些软件包的目录。可以通过指定Python解析器路径的方式直接运行Python,但通常会通过环境变量的方式指定该路径,以方便使用。
通过venv模块或conda,可以方便地在一台PC上创建多个Python环境,并在多版本间切换。PyInstaller可以进一步地将Python程序以及其引用库打包至一个可执行文件中,用户可以双击执行。
使用requirements.txt管理项目依赖
通过requirements.txt
文件,可以统一保存和安装项目所需的库,起到项目依赖管理的作用。
生成requirements.txt
pip3 freeze > requirements.txt
通过pip3 freeze
命令输出项目所需依赖的所有库,并写入名为requirements.txt
的文本文件中。requirements.txt
只是一个约定俗成的命名,其实可以命名为任何名称以及后缀,只要它是有效的文本文件即可,但不建议这样做。
requirements.txt
内容示例如下,指定了包名与版本:
# requirements.txt
numpy==1.23.5
pandas==1.5.2
安装requirements.txt
pip3 install -r requirements.txt
-r 表示read
该命令将自动安装requirements.txt
中指定的每行库及相应版本。
通常,使用requirements.txt
结合Python虚拟环境实现Python运行环境的隔离、部署、打包。
Python虚拟环境
Python的虚拟环境是一个包含了特定版本Python 解析器以及一些软件包的目录,不同的应用程序可以使用不同的虚拟环境,从而解决了依赖冲突问题。并且,虚拟环境中只需要安装应用相关的包或者模块,可以减少包体积,利于部署和分发。
Python3.3及之后的版本自带venv模块用于虚拟环境功能;在VS
Code等IDE中也支持可视化地创建、切换项目的Python环境;而Conda除了提供软件包管理外,也能提供相互隔离的软件环境。
venv模块
创建虚拟环境
通过venv模块(Python3.3及之后的版本自带),在当前目录创建一个虚拟环境文件夹pack_env
python3 -m venv pack_env
就可以得到一个干净的python环境:
pack_env
├── bin
│ ├── Activate.ps1
│ ├── activate
│ ├── activate.csh
│ ├── activate.fish
│ ├── pip
│ ├── pip3
│ ├── pip3.11
│ ├── python -> python3.11
│ ├── python3 -> python3.11
│ └── python3.11 -> /Library/Frameworks/Python.framework/Versions/3.11/bin/python3.11
├── include
│ └── python3.11
├── lib
│ └── python3.11
└── pyvenv.cfg
此时就可以通过指定路径的方式使用该虚拟环境运行python代码:
./pack_env/bin/python3 --version # Python 3.11.7
./pack_env/bin/python3 ./main.py # hello world
安装依赖包
cd pack_env # 进入目录
./bin/pip3 install uuid # 安装单个包
./bin/pip3 install -r ~/code/requirements.txt # 安装requirements.txt
安装完成后就可以在./lib
下看到新增的依赖包文件夹。
打包环境
此时pack_env
就是包含项目依赖包的完整Python环境,可手动压缩该文件夹pack_env.zip
后进行分发。
但venv模块还是手动向,更便利地可以使用conda进行虚拟环境的创建、切换、打包,当然相比于venv模块也会更「重量」。
conda
参考:conda 安装与使用
macOS 彻底删除anaconda
conda 是一个开源的软件包管理系统和环境管理软件,用于安装多个版本的软件包及其依赖关系,并在它们之间轻松切换。
conda 分为 anaconda 和 miniconda。如「Getting started with Anaconda」所述,anaconda 是一个包含了许多常用库的集合版本,并提供名为Anaconda Navigator 的GUI操作界面;miniconda 是精简的CLI命令行版本(只包含conda、python 以及它们所需的包,以及少量常用包),剩余的通过conda install
命令自行安装。
conda原理与venv模块类似,这里就不赘述了。
conda常用的命令
这些CLI命令都可以anaconda界面中找到对应的可视化操作。
# 版本
conda --version
# 查看conda下存在的python环境
conda env list
# 创建一个名为py3.11的环境,其中python使用3.11.11版本
conda create -n py3.11 python=3.11.11
# 创建一个名为py3.11_bk的环境,复制于py3.11环境
conda create -n py3.11_bk --clone py3.11
# 进入py3.11环境
conda activate py3.11
# 查看当前环境下的py包
(conda环境下) conda list
# 进入conda环境后使用Python,输出Python 3.11.11
(conda环境下) python --version
# 进入conda环境后,使用pip安装;anaconda本身只提供部分包,因此conda install远没有pip提供的包多
(conda环境下) pip install numpy==1.26.4
# 从当前环境删除numpy包
(conda环境下) conda remove numpy
# 退出当前环境
conda deactivate
# 删除名为py3.11的环境
conda env remove -n py3.11
# 关闭 Conda 自启动:在安装 Conda 后,每次启动终端都会自动进入 base 环境。为了避免这种情况,可以通过以下方法关闭 Conda 的自启动功能。
conda config --set auto_activate_base false
environment.yml的使用
与requirements.txt
相似,environment.yml
是一个用于定义conda环境的文件,它列出了包括Python解释器版本、库依赖等信息。通过该文件可以在不同的终端上快速的复制相同的环境。
# 生成environment.yml
conda activate my_env # 激活环境
conda env export > environment.yml
# 用environment.yml创建环境
conda env create -f environment.yml
environment.yml
内容示例如下,指定了环境名、镜像、包名与版本:
# requirements.txt
name: my_env
channels:
- defaults
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free
- https://repo.anaconda.com/pkgs/main
- https://repo.anaconda.com/pkgs/r
dependencies:
- ca-certificates=2025.2.25=hca03da5_0
- libcxx=14.0.6=h848a8c0_0
- libffi=3.4.4=hca03da5_1
- ncurses=6.4=h313beb8_0
- openssl=3.0.16=h02f6b3c_0
- pip=24.2=py38hca03da5_0
- python=3.8.20=hb885b13_0
- readline=8.2=h1a28f6b_0
- setuptools=75.1.0=py38hca03da5_0
- sqlite=3.45.3=h80987f9_0
- tk=8.6.14=h6ba3021_0
- wheel=0.44.0=py38hca03da5_0
- xz=5.6.4=h80987f9_1
- zlib=1.2.13=h18a0788_1
- pip:
- certifi==2025.1.31
- charset-normalizer==3.4.1
- cython==3.0.12
- idna==3.10
- numpy==1.24.4
- opencv-python==4.11.0.86
- pillow==10.4.0
- rectpack==0.2.2
- requests==2.32.3
- shapely==2.0.7
- urllib3==2.2.3
- uuid==1.30
- websockets==13.1
- zstandard==0.23.0
conda-pack打包虚拟环境
参考:Conda pack 进行环境打包
通过conda创建的虚拟环境存放在某个目录,如/opt/anaconda3/envs/vris_py_v2
,可以手动将该目录打包压缩用于分发。同时,也可以通过conda-pack进行打包。
安装conda-pack:
conda install conda-pack # 或
pip install conda-pack
打包py3.11环境为压缩包py3.11.tar.gz,输出到指定目录:
conda pack -n py3.11 -o ~/Downloads/py3.11.tar.gz
之后在目标客户的解压该压缩包,即可通过指定路径使用该Python环境:
mkdir ~/Downloads/py3.11
tar -xzvf ~/Downloads/py3.11.tar.gz -C ~/Downloads/py3.11 # 解压
~/Downloads/py3.11/bin/python --version # 输出Python 3.11.11
关于多平台环境
Mac的arm/x86架构问题
Apple M系列芯片采用的是arm架构,而历史Intel芯片采用的是x86架构;Apple为了过渡期的兼容性考虑,arm架构的Mac本身还通过Rosetta转译支持了x86架构应用程序,简而言之,mac_arm可以运行mac_x86的程序,反之不行。
# 在arm下运行 x86架构构建的python以及应用程序,因Rosetta的存在,可以正常运行。
# 但在x86下运行 arm架构构建的python环境,将导致异常:
bad CPU type in executable: python/bin/python3
# 在x86下运行 arm架构构建的应用程序,将导致异常:
(mach-o fiie, but is an incompatible architecture (have 'arm64', need 'x86 64h'or 'x86 64'))
若同时对arm/x86的mac有Python虚拟环境的要求,除了各自准备一台机器分别构建外,还可以通过在arm架构的Mac上构建一个x86虚拟环境用于打包。
# mac_arm 下创建 mac_x86 环境
CONDA_SUBDIR=osx-64 conda create -n py3.11_x86 python==3.11.11
# 激活
conda activate py3.11_x86
# 打印架构,arm/x86架构下分别输出arm64/x86_64
python -c "import platform;print(platform.machine())"
实际上,在conda create
时控制台已经打印了conda所使用的架构
wkw@bogon ~ % CONDA_SUBDIR=osx-64 conda create -n py3.11_x86 python==3.11.11
Retrieving notices: done
Channels:
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free
- defaults
Platform: osx-64 # x86架构下为osx-64,arm架构下为osx-arm64
至于windows,都是x86架构,但是涉及32位还是64位的问题。似乎也可以通过conda对应设置进行虚拟环境的区分,但我的业务不涉及,这里不展开了。该成为历史的就让它成为历史吧。
win7支持的python版本问题
Python官方自3.9起不再支持win7,win7支持的最高版本为Python 3.8.20。
虽然有非官方的魔改版本可以在win7上运行Python3.9甚至3.12版本,但是其稳定性无法得到保障,且安装过程是需要用户介入安装系统补丁的,并不适合作为客户端发布。
我们在验证了程序依赖的Python版本以及依赖库后,使用Python
3.8.20进行开发,来统一win7/win10的程序包。当然,未来若遇到不可抗拒力量要使用Python 3.9以上版本,那我们会放弃win7的维护。
还是那句话,该成为历史的就让它成为历史吧。
PyInstaller打包可执行程序
PyInstaller官网:pyinstaller.org
前文中的虚拟环境以及conda打包,是将python解释器作为运行环境打包(不含用户程序),仍然是通过python运行自己编写的python脚本,如python main.py
。
PyInstaller是将Python程序以及其引用库打包至一个可执行文件中(在Windows下为.exe可执行程序,在Mac下为unix可执行程序),用户可以在不安装Python解释器和其他模块的情况下使用这个程序包,比如双击执行。
PyInstaller支持Python 3.8及以上版本,可以正确打包诸如numpy, matplotlib等主要Python库。
PyInstaller支持Windows, macOS, Linux,但本身不是跨平台编译器,因此需要在对应平台分别运行PyInstaller进行打包。
关于PyInstaller的机制,官方文档讲的很清楚了:
PyInstaller reads a Python script written by you. It analyzes your code to discover every other module and library your script needs in order to execute. Then it collects copies of all those files – including the active Python interpreter! – and puts them with your script in a single folder, or optionally in a single executable file.
梳理几个关键点:
- 它会通过import语句分析代码引用关系,自动将有引用的库进行打包(有些非常规的引用方式可能无法识别,需通过可选参数显式指定)
- 它的输出是基于运行的操作系统、Python版本。执行PyInstaller的Python解释器会作为打包的一部分,因此可以结合conda等虚拟环境进行多版本打包
- 可以打包 One-Folder 或 One-File 模式(即文件夹模式与文件模式)。文件模式在执行时要解压出一个临时文件夹,因此执行速度上略低于文件夹模式
- 打包的app不包含源码,它编译了Python源码,但实际上可被反编译。如果要更彻底地隐藏源码,可以结合 Cython 使用。见我的另一片文章:Python程序的「加密」:Cython编译
安装
执行PyInstaller的Python解释器会作为打包的一部分,比如Python版本是3.11.11,则打包进程序的就是该版本。所以可以结合conda创建虚拟环境,在虚拟环境下安装pyinstaller,进行多版本打包。
pip install pyinstaller
打包
具体用法与参数,见:Using PyInstaller
pyinstaller main.py
默认情况下,会在main.py
同级目录创建./build
目录用于构建过程,并在./dist
输出打包结果。
常用可选项
pyinstaller -D --clean -y --add-data 'fonts/:fonts/' -n myapp main.py
Options | 用途 |
---|---|
-D, --onedir | one-folder模式打包(默认) |
-F, --onefile | one-file模式打包 |
–clean | 在构建前清空缓存和临时文件,可以避免历史版本残留打包 |
-y | 打包结果已存在时,无需确认,直接覆盖 |
-c/-w | 用于控制是否打开控制台或窗口 |
-n, --name NAME | 打包app名称,默认是脚本名称 |
–add-data SOURCE:DEST | 将SOURCE拷贝至DEST,用于添加额外的数据文件或文件夹 |
完
至此,python打包相关实践总结完成。