
CPython importlib.resources 深度指南包内资源的读取、打开与访问【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython导读importlib.resources是 CPython 标准库自 Python 3.7 引入Doc/library/importlib.resources.rst中专门用于访问包内资源的模块。所谓资源指的是随 Python 包一起分发的非 Python 文件例如配置模板、图片、数据表、证书等——包作者在pip install之后依然需要以编程方式读取的那些文件。本文以 CPython 官方文档为骨架结合本仓库中 Lib/importlib/resources/ 的真实源码与 Lib/test/test_importlib/resources/ 的测试用例系统讲解files()与as_file()的 Traversable 体系、函数式 API 的每个函数用法、loader 侧实现ResourceReader的契约以及 zipimport / 命名空间包等特殊场景的行为差异。读完本文你将能够在自己的包中正确、跨分发形态地读取文本与二进制资源并理解其在文件系统包、zip 包、内存包三种载入形态下分别是如何工作的。什么是资源Resourcesimportlib.resources借助 Python 的 import 机制来提供对包package内部资源的访问。这里的资源是与某个模块或包关联的、类似文件的对象。官方文档给出了一个清晰的定义范围资源可能直接位于包内、位于包内某个子目录中或者位于包外部但与模块相邻资源既可以是文本也可以是二进制从技术的角度讲包内的.py源码、__pycache__编译产物、安装产物例如目录中的系统保留文件名见os.path.isreserved都算是该包的 de-facto 资源但在实践中资源主要指包作者有意暴露的非 Python 文件例如 Lib/test/test_importlib/resources/ 测试数据里用到的utf-8.file这类样例数据资源既可以二进制方式打开也可以文本方式打开。一个关键思想是资源像目录里的文件只是一种类比。资源和包并不一定要在文件系统中以真实文件/目录存在——例如使用zipimport时包和它的资源可以直接从 zip 文件中导入。正因为如此访问资源不应依赖真实文件路径这正是importlib.resources存在的意义。官方文档同时给出了两个重要提醒安全模型importlib.resources与内建open函数遵循相同的安全模型把不可信输入传给本模块的函数是不安全的Loader 扩展点希望支持资源读取的 Loader 应实现get_resource_reader(fullname)方法其规格由importlib.resources.abc.ResourceReader定义详见下文Loader 侧的扩展机制一节。面向对象 APIfiles()与as_file()自 Python 3.9 起importlib.resources提供了以 Traversable 协议为核心的推荐 API。相比下面的函数式 API它更接近pathlib的操作习惯且功能更丰富。Anchor资源定位的锚点Anchor表示资源的锚定对象要么是一个模块对象types.ModuleType要么是模块名字符串其类型定义为Union[str, ModuleType]源码中见 Lib/importlib/resources/_common.py#L14-L15。files(anchorNone)importlib.resources.files(anchor: Optional[Anchor] None) - Traversable返回一个代表资源容器可类比目录及其资源的Traversable对象。一个 Traversable 还可以包含其他容器可类比子目录。anchor的解析规则若anchor是包则从该包解析资源若anchor是非包的模块则从与该模块相邻的位置同一包内或包根目录解析资源若省略anchor则使用调用者的模块作为锚点。版本沿革对接口演进非常重要3.9 版本新增3.12 中参数package更名为anchorpackage仍被接受但已弃用3.15 中package参数被完全移除anchor现在可以是非包的模块省略时默认使用调用者的模块需要兼容旧版本 Python 时可考虑使用importlib_resources 5.10backport 独立包提供的兼容接口。从源码看resolve 是一个functools.singledispatch分发函数字符串通过importlib.import_module变成模块对象None则通过_infer_caller()遍历调用栈、找出第一个不在本模块且不是 singledispatchwrapper的调用者来推断调用方模块名。随后 from_package 会通过wrap_spec(package)适配 spec/loader调用spec.loader.get_resource_reader(spec.name)获得 reader并返回reader.files()。也就是说files()最终返回的 Traversable 完全由该包 loader 提供的 reader 决定。as_file(traversable)with importlib.resources.as_file(traversable) as path: # path 是一个 pathlib.Path 对象给定一个代表文件或目录的Traversable通常来自files()返回一个可用于with语句的上下文管理器其产出是一个pathlib.Path对象。退出上下文管理器时会清理为了从 zip 等非文件系统来源解压资源而创建的临时文件或临时目录。当 Traversable 提供的方法read_text等不够用、而确实需要一个真实文件系统路径时例如调用pathlib.Path.stat()、把路径传给只接受真实路径的第三方 C 库才应使用as_file。关键实现细节Lib/importlib/resources/_common.py如果传入的本来就是pathlib.Path例如普通文件系统包由FileReader提供as_file 走 degenerate 分支直接原样返回该 Path不创建任何临时文件——这正是 Lib/test/test_importlib/resources/test_path.py#L27-L34 中test_natural_path所验证的file-system-backed resources do not get the tempdir treatment如果传入的是目录则递归地把整棵目录树写入临时目录再返回_temp_dir/_write_contents如果传入的是 zip 内的文件则用tempfile.mkstemp建立临时文件、写入内容_tempfile并在退出with时调用os.remove清理即使文件在 with 块内被提前删除FileNotFoundError也会被吞掉见 test_path.py 的test_remove_in_context_manager。3.12 起支持traversable代表目录的情形。Traversable 协议与路径操作Traversable 是标注了runtime_checkable的 Protocol实现了pathlib.Path的、适用于目录遍历和文件打开的子集。其成员包括iterdir()产出其中的 Traversableread_bytes()/read_text(encodingNone, errorsNone)直接读取内容is_dir()/is_file()判断是容器还是文件joinpath(*descendants)拼接子路径。每个 descendant 是相对自身的路径片段各片段可以包含以/posixpath.sep分隔的多级。它通过遍历iterdir()逐级匹配名称实现找不到目标时抛出TraversalError__truediv__因此resources.files(pkg) / data / file.txt的写法与joinpath等价测试中大量使用这种写法open(moder, ...)模式支持r或rb文本模式接受encoding等参数语义同pathlib.Path.openname属性不含父级引用的基本名称。组合使用示例from importlib import resources # 枚举包内的顶层条目文件与目录名str for name in resources.files(my_pkg).iterdir(): print(name.name) # 用 / 风格拼接子路径并直接读取 data resources.files(my_pkg).joinpath(data, config.json).read_text(encodingutf-8) # 等价写法 data (resources.files(my_pkg) / data / config.json).read_text(encodingutf-8)函数式 APIFunctional API为了向后兼容importlib.resources提供了一组简化的、向后兼容的辅助函数每个常见操作一次函数调用即可完成。它们在 Lib/importlib/resources/_functional.py 中实现并被 Lib/importlib/resources/init.py 统一导出。公共约定对所有下列函数成立anchor是一个Anchor语义与files()相同但不同于files这里不能省略 anchor源码中 anchor 为None会直接抛出TypeError: anchor must be module or string, got None见 Lib/importlib/resources/_functional.py#L81-L84path_names是资源路径名的组成部分相对于 anchor。例如读取名为info.txt的文本importlib.resources.read_text(my_module, info.txt)与Traversable.joinpath相同各组成部分之间应使用正斜杠/作为路径分隔符。例如下面两种写法是等价的importlib.resources.read_binary(my_module, pics/painting.png) importlib.resources.read_binary(my_module, pics, painting.png)兼容性限制出于向后兼容原因如果传入了多个path_names读取文本的函数要求显式给出encoding参数。例如读取info/chapter1.txt需写作importlib.resources.read_text(my_module, info, chapter1.txt, encodingutf-8)在源码层面这个限制由 _get_encoding_arg 实现当encoding缺省且path_names多于 1 个时抛出TypeError(encoding argument required with multiple path names)。该限制计划在 Python 3.15 移除。所有函数内部最终都落到files(anchor).joinpath(*path_names)返回的 Traversable 上_get_resource因此在 zip 包上同样可用。自 3.13 起这些函数均支持多个path_names。open_binary(anchor, *path_names)以二进制读取方式打开指定资源返回typing.BinaryIO二进制读取流。大致等价于files(anchor).joinpath(*path_names).open(rb)open_text(anchor, *path_names, encodingutf-8, errorsstrict)以文本读取方式打开指定资源。默认按严格 UTF-8读取encoding与errors的含义与内建open一致。返回typing.TextIO。大致等价于files(anchor).joinpath(*path_names).open(r, encodingencoding)向后兼容的注意点存在多个path_names时encoding必须显式给出且自 3.13 起encoding和errors必须以关键字参数形式提供。该限制计划在 Python 3.15 移除。read_binary(anchor, *path_names)读取并返回指定资源的全部内容结果为bytes。大致等价于files(anchor).joinpath(*path_names).read_bytes()read_text(anchor, *path_names, encodingutf-8, errorsstrict)读取并返回指定资源的全部内容结果为str。默认严格 UTF-8encoding/errors语义同内建open。存在多个path_names时encoding必须显式给出计划 3.15 移除该限制。大致等价于files(anchor).joinpath(*path_names).read_text(encodingencoding)path(anchor, *path_names)提供资源对应的真实文件系统路径。返回一个上下文管理器用于with其产出的pathlib.Path即真实路径退出时清理为从 zip 等来源解压创建的临时文件。例如pathlib.Path.stat需要真实路径可以这样用with importlib.resources.path(anchor, resource.txt) as fspath: result fspath.stat()该函数本质上就是as_file(files(anchor).joinpath(*path_names))注意与as_file一样当资源天然就在文件系统中时path不会产生临时副本普通文件系统包的FileReader.resource_path会直接返回文件系统路径以避免临时复制见 Lib/importlib/resources/readers.py#L21-L34。is_resource(anchor, *path_names)若指定资源存在则返回True否则False。目录不被视为资源。大致等价于files(anchor).joinpath(*path_names).is_file()源码实现还通过捕获TraversalError兜底返回False保证对不存在路径的查询不抛异常Lib/importlib/resources/_functional.py#L40-L48。contents(anchor, *path_names)返回对包内或路径内命名条目的可迭代对象可迭代对象以str形式产出资源名如文件和非资源名如目录并且不递归进子目录。大致等价于for resource in files(anchor).joinpath(*path_names).iterdir(): yield resource.name弃用警告自Python 3.11起contents已被弃用官方建议改用上面的iterdir()写法——它对结果有更多控制权、功能更丰富。源码在调用时会发出DeprecationWarningLib/importlib/resources/_functional.py#L51-L63。本文前文的files()API 一节即推荐用Traversable.iterdir()取代它。实际调用链小结下表汇总各函数式 API 与其面向对象实现的对应关系便于查阅函数式 API底层等价实现open_binary(a, *n)files(a).joinpath(*n).open(rb)open_text(a, *n, ...)files(a).joinpath(*n).open(r, ...)read_binary(a, *n)files(a).joinpath(*n).read_bytes()read_text(a, *n, ...)files(a).joinpath(*n).read_text(...)path(a, *n)as_file(files(a).joinpath(*n))is_resource(a, *n)files(a).joinpath(*n).is_file()contents(a, *n)对files(a).joinpath(*n).iterdir()取.nameLoader 侧的扩展机制ResourceReader与get_resource_reader要实现完整的资源读取支持包依赖的 loader 是关键。官方文档明确希望支持资源读取的 loader 应实现get_resource_reader(fullname)其规格见importlib.resources.abc.ResourceReader。ResourceReader抽象基类Lib/importlib/resources/abc.py#L24-L63 中定义的ResourceReadermetaclassabc.ABCMeta要求实现四个方法open_resource(resource)返回用于二进制读取的已打开 file-like 对象。resource仅代表一个文件名找不到时抛FileNotFoundError基类刻意抛FileNotFoundError而非NotImplementedErrorresource_path(resource)返回指定资源在文件系统上的路径文件系统上不存在则抛FileNotFoundErroris_resource(path)命名路径是否是资源——文件是资源目录不是contents()返回包内条目的可迭代对象。TraversableResources与内置实现TraversableResourcesabc.py#L169-L189是提供 traversable 资源的接口额外要求files()返回包的 Traversable其余方法都以files()为基准实现FileReaderreaders.py#L21-L34文件系统 loader 的 readerfiles()直接返回包目录对应的pathlib.PathZipReaderreaders.py#L37-L60zip 包 loader 的 readerfiles()返回zipfile.Path(archive, prefix)从而让 API 在 zipimport 场景下同样可用它还对zipfile.Path.is_file在路径不存在时返回True的怪癖做了修正is_resource同时检查is_file()与exists()NamespaceReader/MultiplexedPathreaders.py#L63-L185处理命名空间包可能多宿主multihomed在多个目录/zip 的情况——对每个条目依次尝试解析为文件系统目录或 zip 内部路径再把多个 Traversable 合并成一个逻辑视图。在 CPython 当前实现里from_package 会先通过 _adapters.py 的wrap_spec把包的 spec 包装为SpecLoaderAdapter使其拥有TraversableResourcesLoader提供的get_resource_reader若底层 loader 本身没有 reader 或 reader 不支持files()则由CompatibilityFiles适配出一个基于ResourceReader四方法的兼容视图并区分可读的SpecPath/ChildPath与不可读的OrphanPath。对于自定义 loader接入方式即实现get_resource_readerclass MyLoader(importlib.abc.Loader): def get_resource_reader(self, fullname): # 返回一个实现了 TraversableResources / ResourceReader 接口的对象 return MyResourceReader(...)标准库自带 loader 之外主流打包工具例如 setuptools 构建的普通文件系统包默认走文件系统路径天然被FileReader支持无需额外工作。跨分发形态文件系统、zip 与内存包仓库测试 Lib/test/test_importlib/resources/ 把使用场景按载入形态分成DiskSetup磁盘文件系统、ZipSetupzip 导入、以及内存包三类。理解importlib.resources的价值核心在于无论资源以何种形态分发调用方的代码都不需要变化。文件系统包普通pip install后即是此形态。files()返回pathlib.Path所有读写直通文件系统as_file/path零拷贝zip 包通过zipimport把.zip加入sys.path即可导入。此时资源没有真实文件系统路径read_*直接在 zip 成员上工作只有需要真实路径时as_file/path才会把内容解压到tempfile创建的临时位置退出with即清理内存包loader 把包内容保存在内存对象中。测试中的PathMemoryTests用io.BytesIO构造一个只存在于内存的包__spec__.origin None、has_location False验证资源读取 API 在无文件位置的情况下依然可用——这印证了文档中资源不必是物理文件的论断。这要求包作者遵循一条实践准则不要假设资源一定有可用的__file__路径而是始终经由importlib.resources的抽象层访问资源文件。对于更老版本 Python 的兼容官方文档建议参考独立 backport 包importlib_resources其文档还包含从pkg_resources迁移的专门指引本模块源码头部也注明与 PyPI 上的importlib_resources共享同一套代码库见 Lib/importlib/resources/init.py 顶部注释。实战建议与常见误区在函数/方法内部省略 anchorfiles()省略anchor时使用调用者的模块作为锚点即引用定义处所在模块同目录的资源在使用前应确认该模块已通过 import 系统加载拥有非None的__spec__。若对象的__spec__为None例如直接运行__main__的某些场景源码中的_assert_spec会给出明确报错提示Lib/importlib/resources/_common.py#L74-L85。目录不是资源is_resource对目录返回FalseTraversable.is_file()同理要判断某个容器/文件是否存在请综合使用is_file()、is_dir()或捕获TraversalError。不要手动拼接真实文件路径一旦包被塞进 zip 或以其他非常规方式分发基于__file__的路径拼接就会失效应始终经由files(anchor).joinpath(...)或函数式 API。需要真实路径时用上下文管理器path/as_file可能创建临时文件/目录务必用with语句包裹确保退出即清理如果资源本来就在文件系统上则不会有额外开销。使用/而非os.seppath_names与joinpath内部通过PurePosixPath解析、统一以正斜杠分隔见 abc.py#L114-L137跨平台写代码时不要引入平台相关分隔符。新代码优先使用 Traversable APIfiles()iterdir()/read_text()等是现行推荐接口contents()已弃用package位置参数自 3.15 起不复存在。延伸阅读模块公共导出与__all__本模块实际导出的名称Anchor、Traversable等见 Lib/importlib/resources/init.pyTraversable 协议与 ResourceReader 契约可继续阅读其定义模块 Lib/importlib/resources/abc.py各分发形态的行为验证仓库内置了覆盖磁盘、zip、内存、自定义 loader、命名空间包等场景的完整测试集位于 Lib/test/test_importlib/resources/例如 test_files.py、test_path.py、test_reader.py是学习边界行为的最佳教材资源打包与发布的配套机制本仓库另有 Lib/zipimport.pyzip 导入实现、Modules/zipimport.c 与打包工具的 zip_safe/包数据配置可与本文主题配合阅读。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考