Python 能直接 import 一个 zip 里的代码,.pyz 可执行文件和很多打包工具都靠它。但 zip 不是目录:哪些事情能做、哪些不能、快不快,这篇都拿真实的 zip 包测了一遍。
实验脚本(节选) # src/mypkg/{__init__.py, mod.py, data.txt} 压成 app.zip sys.path.insert(0, os.path.abspath("app.zip")) import mypkg, mypkg.mod print(mypkg.mod.__file__, type(mypkg.__loader__))
zip 里的文件: ['mypkg/__init__.py', 'mypkg/data.txt', 'mypkg/mod.py'] mypkg.__file__ = <临时目录>/app.zip/mypkg/__init__.py mypkg.mod.__file__ = <临时目录>/app.zip/mypkg/mod.py mypkg.__loader__ = <class 'zipimport.zipimporter'> mypkg.__spec__.origin = <临时目录>/app.zip/mypkg/__init__.py sys.path_importer_cache[zip] 的类型 = <class 'zipimport.zipimporter'> sys.path_hooks = ['zipimporter', 'path_hook_for_FileFinder'] import 之后 zip 里的文件: ['mypkg/__init__.py', 'mypkg/data.txt', 'mypkg/mod.py'] → 变化了吗? False zip 旁边有 __pycache__ 吗? False | src 目录有吗? False
路径里的 app.zip/mypkg/mod.py 把 zip 当成了目录。sys.path_hooks 里有两个 hook:zipimporter 认得 zip 文件,path_hook_for_FileFinder 认得普通目录;sys.path_importer_cache 里 app.zip 对应的就是 zipimporter 对象。
还有一处值得注意:import 之后 zip 文件的内容没变,zip 旁边也没有出现 __pycache__——zipimport 从不往 zip 里写字节码缓存。
open(dirname(__file__)/data.txt) → NotADirectoryError: [Errno 20] Not a directory: '<临时目录>/app.zip/mypkg/data.txt'
pkgutil.get_data('mypkg', 'data.txt') → b'hello from data.txt\n'
importlib.resources.files('mypkg').joinpath('data.txt').read_text() → 'hello from data.txt\n'因为 __file__ 指向的是「zip 里的一个虚拟路径」,操作系统不认识,直接 open 得到 NotADirectoryError。走 pkgutil.get_data 或 importlib.resources.files 就没问题,这两个接口会通过 loader 读。写包时如果要兼容 zip 分发,数据文件一律用这两个接口读。
zip 里放了两份真实 .so: array.cpython-314-darwin.so 和改名的 myext.cpython-314-darwin.so; zip 排在 sys.path 第一位 import array → 实际来自 /opt/homebrew/Cellar/python@3.14/3.14.7/Frameworks/Python.framework/Versions/3.14/lib/python3.14/lib-dynload/array.cpython-314-darwin.so import myext → ModuleNotFoundError: No module named 'myext'
第一个实验里,zip 里放了真实的 array.cpython-314-darwin.so 并排在路径最前,import array 依然来自标准库自带的目录——zip 里的那份被静默忽略。第二个实验把 .so 改名为 myext,只有 zip 里有它,结果是 ModuleNotFoundError——zipimporter 根本不把 .so 当作可导入的模块。带 C 扩展的包(numpy 之类)不能整个塞进 zip 运行。
head -c 40 app.pyz = b'#!/usr/bin/env python3\nPK\x03\x04\x14\x00\x00\x00\x00\x00\xcc\xaa=];\xeb-' python3 app.pyz → __main__ 在 zip 里运行, mypkg.VERSION = 1.0 | __file__ = ['app.pyz', '__main__.py']
python -m zipapp 只是把目录打包成 zip,并在开头加一行 #! 解释器行(输出里能看到它在 PK 头之前)。之所以能直接执行,是因为 Python 遇到 zip 文件名会把它当成 sys.path 条目,并运行其中的 __main__.py。
源文件行数: 8999 | big_src.zip 大小: 23219 字节 | big_pyc.zip: 46621 字节 目录 + __pycache__ import 用时(9 次取中位数): 1.42 ms zip 里放 .py import 用时(9 次取中位数): 49.78 ms zip 里放 .pyc import 用时(9 次取中位数): 3.07 ms zip 里放 .py 之后再跑, zip 内容有变化吗? ['bigmod/__init__.py']
同一份 8999 行的模块,在三种放法下导入耗时(各 9 次取中位数):目录里已有 __pycache__ 只要 1.42 ms;zip 里放 .py 要 49.8 ms,因为每个新进程都要重新编译一遍;zip 里放预先编译好的 .pyc 是 3.07 ms。所以对启动速度有要求的 zip 分发,应该把 .pyc 打进去(本实验里的 .pyc 是用 py_compile 生成的)。
zipimport、zipapp、pkgutil、importlib.resources