bigywater.tech
Python · 80

zipimport:Python 如何直接从 zip 包加载代码

Python 能直接 import 一个 zip 里的代码,.pyz 可执行文件和很多打包工具都靠它。但 zip 不是目录:哪些事情能做、哪些不能、快不快,这篇都拿真实的 zip 包测了一遍。

zipimporter
导入 zip 里的模块时,__loader__ 的类型
50 ms vs 1.4 ms
9000 行模块:zip 里放 .py 与目录(有 pyc 缓存)
NotADirectoryError
相对 __file__ 读 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

验证一:zip 在导入系统里就是一个「目录」

路径里的 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 里写字节码缓存。

验证二:包里的数据文件不能相对 __file__ 找

实测 · 本机执行输出
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 分发,数据文件一律用这两个接口读。

验证三:扩展模块(.so)不能从 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 运行。

验证四:python app.pyz

实测 · 本机执行输出
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。

验证五:zip 里放 .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 生成的)。

分步演示:一次 zip 导入,和三种放法的速度

数据是上面实验的实测结果
zipimport 的导入路径
此刻的阶段 / 数据

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7,标准库 zipimport、zipapp、pkgutil、importlib.resources
    • 没有验证:zip64、有密码的 zip、zip 里同时有 .py 和 .pyc 时的优先级、Windows 上的路径分隔符差异
    京ICP备2026067502号-1京公网安备11010502063487号