【Python】.pyz:源码与依赖打包
.pyz
是 Python 的可执行压缩包格式,本质是一个 ZIP 文件,包含 Python 源码和依赖模块,解释器可直接运行,无需解压或安装。从 Python 3.5+ 开始,官方通过zipapp
模块支持创建和运行.pyz
文件。本教程介绍如何使用脚本自动打包项目源码和依赖为仅含.pyc
的.pyz
文件,优化分发和执行效率。
打包原理
.pyz
文件是一个 ZIP 压缩包,包含以下内容:
- 项目文件:源码(
.py
、.pyc
、.pyd
)及依赖模块。 - 入口文件:通常为
__main__.py
,Python 运行.pyz
时默认执行其中的main
函数。 - 运行机制:Python 解释器自动解压
.pyz
并执行入口函数,支持跨平台分发。
创建简单的 .pyz 包
以下通过示例项目说明如何打包为 .pyz
文件,支持自动检测 __main__.py
或手动指定入口。
示例项目结构
myproject/
├── main.py
└── module/└── utils.py
目标:将 myproject
打包为 myproject.pyz
,自动使用 __main__.py
或手动指定入口。
准备入口文件
Python 默认运行 .pyz
中的 __main__.py
。你可以:
-
重命名或创建
__main__.py
:将main.py
重命名为__main__.py
,或新建__main__.py
,示例内容:# myproject/__main__.py from module import utilsdef main():print("Hello from pyz!")utils.do_something()if __name__ == '__main__':main()
-
直接使用现有
__main__.py
:如果源码目录已包含__main__.py
,无需额外定义main
函数或指定入口。
使用 zipapp
打包
在 myproject
的上级目录运行:
python -m zipapp myproject -o myproject.pyz
-
自动入口:如果
myproject
包含__main__.py
,zipapp
默认执行其__main__
块,无需-m
参数。 -
手动指定入口:若无
__main__.py
或需指定其他入口,使用-m
参数,例如:python -m zipapp myproject -o myproject.pyz -m "main:main"
-
参数说明:
myproject
:源码目录。-o myproject.pyz
:输出文件名。-m "module:function"
:手动指定入口模块和函数(如main.py
的main
函数)。
-
结果:生成
myproject.pyz
,可通过python myproject.pyz
运行。
高级打包脚本
手动打包适用于简单项目,但复杂项目需包含依赖(如 site-packages
)并优化为 .pyc
文件以减小体积和保护源码。以下脚本自动完成这些任务。
脚本功能
- 输入:
--site-packages
:依赖目录,默认自动检测虚拟环境或系统的site-packages
。--src
:源码目录,默认当前目录。--out
:输出.pyz
文件名,默认使用源码目录名加.pyz
。--entry
:入口函数,格式module:function
,默认main:main
。
- 流程:
- 复制
site-packages
和源码(.py
、.pyc
、.pyd
)到临时目录。 - 排除无关文件(如
_virtualenv.py
、_virtualenv.pth
)和目录(如__pycache__
)。 - 编译
.py
为.pyc
,删除.py
文件。 - 打包为压缩
.pyz
文件。
- 复制
脚本代码
import os
import sys
import shutil
import zipapp
import argparse
import compileall
import tempfile
import site# 获取 site-packages 路径,优先选择标准路径或当前目录
candidates = [p for p in site.getsitepackages() if p.endswith("site-packages")] if hasattr(site, "getsitepackages") else []
default_site_packages = candidates[0] if candidates else next((p for p in sys.path if "site-packages" in p), os.path.abspath("."))
# 如果路径非 site-packages,尝试拼接虚拟环境标准位置
if os.path.basename(default_site_packages) != "site-packages":candidate = os.path.join(default_site_packages, "Lib", "site-packages")if os.path.exists(candidate):default_site_packages = candidate# 设置命令行参数解析
parser = argparse.ArgumentParser(description="打包 site-packages 和源码为仅含 pyc 的压缩 pyz,排除无用文件夹")
parser.add_argument("--site-packages", default=default_site_packages, help="虚拟环境或系统的 site-packages,默认当前解释器对应路径")
parser.add_argument("--src", default=".", help="源码目录,默认当前目录")
parser.add_argument("--out", default=None, help="输出 pyz 文件名,默认源码目录名加 .pyz")
parser.add_argument("--entry", default="main:main", help="入口函数,格式 module:function,默认 main:main")
args = parser.parse_args()# 设置默认输出文件名基于源码目录
if args.out is None:args.out = os.path.basename(os.path.abspath(args.src)) + ".pyz"# 检查 site-packages 目录是否存在
site_packages_dir = args.site_packages
if not os.path.exists(site_packages_dir):print(f"错误: 找不到 site-packages 目录:{site_packages_dir}", file=sys.stderr)sys.exit(1)
print(f"指定的 site-packages 目录:{site_packages_dir}")# 定义排除的文件和目录
exclude_files = {"_virtualenv.py", "_virtualenv.pth"}
exclude_dirs = {"__pycache__"}# 创建临时目录用于构建
with tempfile.TemporaryDirectory() as build_dir:site_dir = os.path.join(build_dir, "site")os.makedirs(site_dir, exist_ok=True)# 复制 site-packages 中的文件和目录for item in os.listdir(site_packages_dir):if item in exclude_files:print(f"跳过文件:{item}")continues = os.path.join(site_packages_dir, item)d = os.path.join(site_dir, item)if os.path.isdir(s):if item in exclude_dirs:print(f"跳过目录:{item}")continueshutil.copytree(s, d)print(f"复制目录:{s} -> {d}")else:shutil.copy2(s, d)print(f"复制文件:{s} -> {d}")# 复制源码 .py、.pyc 和 .pyd 文件for f in os.listdir(args.src):if f.endswith((".py", ".pyc", ".pyd")) and f != os.path.basename(__file__):shutil.copy2(os.path.join(args.src, f), os.path.join(site_dir, f))print(f"复制源码文件:{f}")# 编译 .py 文件为 .pycprint("编译 .py 文件生成 .pyc ...")compileall.compile_dir(site_dir, force=True, legacy=True, quiet=1)# 删除源码 .py 文件for root, _, files in os.walk(site_dir):for file in files:if file.endswith(".py"):os.remove(os.path.join(root, file))print(f"删除源码文件:{os.path.join(root, file)}")# 打包为压缩 pyz 文件print(f"打包为压缩 pyz 文件:{args.out},入口函数:{args.entry}")zipapp.create_archive(site_dir, args.out, main=args.entry, interpreter=sys.executable, compressed=True)print(f"✅ 打包完成:{args.out}")
使用示例
假设项目结构如下:
myapp/
├── __main__.py
├── utils.py
└── lib/└── helper.py
运行脚本:
python pack.py --src myapp --site-packages /path/to/venv/lib/python3.8/site-packages
- 结果:生成
myapp.pyz
,包含site-packages
和源码的.pyc
文件,入口为main:main
(需确保__main__.py
中有main
函数)。 - 运行:
python myapp.pyz
。
总结
本教程展示了如何通过 zipapp
模块和自定义脚本将 Python 项目打包为 .pyz
文件。简单项目可直接使用 python -m zipapp
,而复杂项目可利用提供的脚本,自动化处理依赖、编译和打包,生成高效、可分发的 .pyz
文件。