Python 包资源读取:模块能从 ZIP 导入,旁边的模板为什么不能当普通路径打开

前天 3阅读

开发目录里,按模块的 __file__ 拼出模板路径一直可用;把包放进 ZIP 后,模块仍能导入,模板却打不开。原因是导入系统能够理解归档内部位置,而普通文件系统接口未必理解。资源属于包,并不保证它已经是一份可直接打开的磁盘文件。

本文在 CPython 3.12.14 上执行,只在临时目录制作一个自包含的 ZIP 包,不安装依赖,也不读取现有项目。保存为 demo.py,再运行 python demo.py。特意选择标准 ZIP 导入器,避免把“开发机器上恰好是目录”误当成所有部署方式的共同条件。

让读取入口以包为锚点

files 返回支持遍历和读取的 Traversable 对象,调用 joinpath 找到包内模板,然后直接 read_text。它提供资源访问能力,但不是必须转换成 pathlib.Path 的占位符。只需要内容的调用方,拿到字符串或字节通常已够用,可以让加载器处理底层位置。

Python 包资源读取:模块能从 ZIP 导入,旁边的模板为什么不能当普通路径打开

AI概念示意图:归档盒中的书页可以被直接读取,另一条临时桥将资源交给必须使用真实路径的工具。图片不是文件管理器截图。

import importlib
from importlib.resources import files, as_file
from pathlib import Path
import sys
import tempfile
import zipfile

name = "batch22_resource_demo"
with tempfile.TemporaryDirectory() as folder:
    archive = Path(folder) / "bundle.zip"
    with zipfile.ZipFile(archive, "w") as bundle:
        bundle.writestr(name + "/__init__.py", "")
        bundle.writestr(name + "/template.txt", "hello from archive")
    sys.path.insert(0, str(archive))
    try:
        package = importlib.import_module(name)
        ordinary = Path(package.__file__).with_name("template.txt")
        assert not ordinary.exists()
        print("ordinary path:", ordinary.exists())
        resource = files(package).joinpath("template.txt")
        assert resource.read_text(encoding="utf-8") == "hello from archive"
        print("resource:", resource.read_text(encoding="utf-8"))
        with as_file(resource) as extracted:
            assert extracted.is_file()
            assert extracted.read_text(encoding="utf-8") == "hello from archive"
        assert not extracted.exists()
        print("extracted removed:", not extracted.exists())
    finally:
        sys.path.remove(str(archive))
        sys.modules.pop(name, None)

包内位置与磁盘路径分别验收

前两行输出 ordinary path: False 和 resource: hello from archive。通过 __file__ 推出来的位置不是真实文件,但资源读取成功。测试同时检查这两个事实,才能证明代码确实走了归档资源入口,而不是某个同名开发目录抢先被导入。

本例把 ZIP 放到 sys.path 首位,并使用专门的包名;退出时恢复搜索路径并移除自己的模块缓存条目。实际项目应由正常打包和导入流程提供包,不必在业务函数里反复修改 sys.path。资源也必须进入发布产物,读取 API 不会替构建工具补上遗漏的文件。

只有需要文件路径时才展开

有些第三方库只接受路径,不能接受字节或文件对象。这时把资源交给 as_file,在 with 内取得实际 Path。对本例的 ZIP 成员,它会准备临时文件;代码在作用范围内读取成功,退出后断言文件已不存在,最后打印 extracted removed: True。

清理保证针对为资源访问创建的临时产物。如果包本来就位于普通目录,as_file 可能直接提供已有路径,不应期待它退出后删除源文件。也不要把临时 Path 返回给稍后执行的任务;应在作用范围内完成消费,或由调用方明确保存一份自己负责管理的副本。

如果库会延迟打开路径,函数返回不代表资源已经用完。例如先创建一个延迟加载对象,再立即退出 with,真正读取时仍可能失败。应查清消费者何时完成访问,把上下文覆盖到那个时刻;不能只因构造对象没有报错就认为文件可以清理。

部署验收要包含资源本身

建议为发布包保留两种小测试:直接从正常安装目录读取,以及从支持资源协议的归档布局读取,并核对内容。测试只能证明采用的加载器与布局,不能推广到所有自定义加载器。本文演示文件资源;目录展开能力与不同 Python 版本有关,需要另查所用版本的接口。

资源名也应来自明确的内部清单,不把外部输入未经约束地拼进查找路径。包资源接口解决的是定位与读取方式,不负责判断某个名字是否允许被访问。把包装入发布包、加载器能够读取、消费者已经用完三件事分别确认,部署故障才有明确的检查位置。

参考资料

Python 官方文档:importlib.resources

Python 官方文档:zipimport

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。