
CPython 资源访问抽象层importlib.resources.abc详解ResourceReader、Traversable 与 TraversableResources【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本篇文章围绕 CPython 标准库模块Doc/library/importlib.resources.abc.rst展开系统讲解从包package中读取“资源”如随包分发的数据文件所需的三套抽象基类/协议已被取代的ResourceReader、pathlib.Path风格的Traversable以及现役推荐接口TraversableResources。无论包与数据文件是存放在普通文件系统、zip 归档还是命名空间包的多个目录中这些 ABC 都向上层importlib.resources.files()提供统一的访问入口读者读完本文后既能理解标准 loader 的既有实现也能照着接口编写自己的资源加载器。模块定位与设计目标importlib.resources.abc是 Python 3.11 新增的标准库模块见 abc.py其__all__仅导出三个名字__all__ [ResourceReader, Traversable, TraversableResources]整个模块解决的问题非常集中屏蔽“包及其资源存储方式”的差异。从该 ABC 的视角看一个resource资源是指随包一起分发的二进制工件binary artifact典型形态是与包的__init__.py相邻的数据文件。调用方不关心这个包是装在 zip 文件里还是散落在文件系统目录中——ResourceReader的职责正是让这种读取方式上的差异对用户透明。与纯 Python 实现相关其 C 语言孪生/引导逻辑可进一步参见 Lib/importlib/readers.py公开的importlib.readers入口与下层引导模块 Lib/importlib/_bootstrap_external.py。使用本模块需要满足一个前提导入系统已经能正常加载该包。资源读取依赖“先 import 包再读包内文件”的次序若想访问尚未被导入的包的内容请先通过importlib.import_module()完成导入。关键抽象包即“目录”资源即“文件名”在理解三个类之前必须先掌握本模块贯穿始终的隐喻包package扮演“目录”的角色实例化的 reader 被要求一对一直接对应某个具体包而不是一个模块也不能同时代表多个包资源resource在方法参数层面只表示一个“文件名”参数应当是一个 path-like object资源参数不允许包含子目录路径因为包的存放位置本身就充当着顶层“目录”子路径访问交由 3.11 后引入的Traversable.joinpath()/Traversable树形遍历去完成。ResourceReader面向“扁平资源”的旧版抽象基类ResourceReader是这一组 ABC 中历史最久的成员。它让 loader 具备“读取资源”的能力但从 3.12 起被标记为deprecated官方明确要求改用TraversableResources。从文档与源码均可看到其废弃声明.. deprecated:: 3.12Use TraversableResources instead。**新代码一律实现 TraversableResources无需再直接实现 ResourceReader。**与加载器的契约get_resource_reader(fullname)希望支持资源读取的 loader 需要提供get_resource_reader(fullname)方法返回一个实现了本 ABC 接口的对象契约包括情况返回值要求fullname指定的模块是包返回实现了ResourceReader或其子类接口的对象fullname指定的模块不是包返回None在实现层面该契约可参考 Lib/importlib/resources/_common.py 的get_resource_reader()辅助函数它通过getattr(spec.loader, get_resource_reader, None)探测 loader 能力再以spec.name调用之。四个抽象方法的语义方法语义与错误约定open_resource(resource)返回一个已打开、用于二进制读取的 file-like 对象找不到资源时抛FileNotFoundErrorresource_path(resource)返回资源对应的文件系统路径若资源并不真实存在于文件系统抛FileNotFoundErroris_resource(path)判断指定path是否被视为资源返回True/Falsepath不存在时抛FileNotFoundError历史版本中该参数名为name后更名为pathcontents()返回一个字符串迭代器遍历包的顶层内容对contents()有两个重要约束值得展开不要求迭代出的每个名字都是真正的资源——允许返回那些对is_resource()而言为False的名字之所以如此宽松是为了照顾“存储方式已知”的场景例如当确定包与资源都存放在文件系统上时允许把子目录名也返回出来调用方可以直接拼接这些名字去访问真实路径。而“抽象方法返回空迭代器”只是基类给出的默认占位行为。源码中的默认实现细节ResourceReader在 Lib/importlib/resources/abc.py 中的四个抽象方法并非raise NotImplementedError而是刻意抛FileNotFoundErrorabc.abstractmethod def open_resource(self, resource: Text) - BinaryIO: ... # This deliberately raises FileNotFoundError instead of # NotImplementedError so that if this method is accidentally called, # itll still do the right thing. raise FileNotFoundError源码注释解释得很清楚一旦这些“应被覆盖却未被覆盖”的方法被意外调用抛FileNotFoundError能让调用方得到符合语义的错误而不是令人困惑的“未实现”异常。Traversablepathlib.Path 风格的可遍历句柄Traversable是文档所称的、“具备 pathlib.Path 方法子集的、适合遍历目录与打开文件的对象”。它是从 3.11 起支撑importlib.resources.files()返回值的核心抽象。代码中它是一个使用runtime_checkable声明的Protocol参见 Lib/importlib/resources/abc.py因此可以在运行时用isinstance(obj, Traversable)做检查。若需要把Traversable对象“落回”文件系统请使用importlib.resources.as_file详情见下文“as_file 与临时文件”小节。接口成员一览Traversable要求实现方提供以下能力成员类型/语义name只读属性对象的基名不含任何父级路径引用iterdir()产出本对象下的Traversable子对象is_dir()返回True表示自身是目录is_file()返回True表示自身是文件joinpath(*pathsegments)按路径段向下遍历返回Traversable结果__truediv__(child)return self.joinpath(child)与joinpath等价open(moder, *args, **kwargs)打开句柄用于读取行为同pathlib.Path.open派生实现不需要自己编写读取文本/字节的代码因为协议基类提供了模板方法式的默认实现def read_bytes(self) - bytes: with self.open(rb) as strm: return strm.read() def read_text(self, encodingNone, errorsNone) - str: with self.open(encodingencoding, errorserrors) as strm: return strm.read()joinpath的多段参数与兼容性注意事项joinpath是 3.11 的增强点.. versionchanged:: 3.11现在接受多个pathsegments参数每个段内允许包含以正斜杠/即posixpath.sep分隔的多级名字因此下面两种写法等价files.joinpath(subdir, subsubdir, file.txt) files.joinpath(subdir/subsuddir/file.txt)注意原文档示例保留了笔误写法subsuddir实际目标目录名请以真实文件系统为准。兼容性提醒部分Traversable实现尚未升级到协议最新版即早期只接受单一child参数。为与这类旧实现兼容每个joinpath调用应只传一个不含路径分隔符的段通过链式调用逐层下钻files.joinpath(subdir).joinpath(subsubdir).joinpath(file.txt)open的模式与文本编码参数open()的mode只允许两种取值r以文本模式打开rb以二进制模式打开。当以文本模式打开时它接受io.TextIOWrapper支持的编码类关键字参数例如encoding...、errors...这些参数会被原样透传。正因为如此协议基类中的read_text签名是read_text(encodingNone, errorsNone)。源码中joinpath的默认遍历实现值得单独指出尽管文档示例把它们当作抽象接口实际 Lib/importlib/resources/abc.py 中joinpath与__truediv__带有默认实现可被任何只实现了iterdir()/name的Traversable直接复用def joinpath(self, *descendants): if not descendants: return self names itertools.chain.from_iterable( path.parts for path in map(pathlib.PurePosixPath, descendants) ) target next(names) matches (t for t in self.iterdir() if t.name target) try: match next(matches) except StopIteration: raise TraversalError(Target not found during traversal., target, list(names)) return match.joinpath(*names)其内部先把各段用pathlib.PurePosixPath按/切分再逐级在iterdir()结果中按name匹配若途中找不到目标会抛TraversalError该异常类型同样定义于 Lib/importlib/resources/abc.py。多个未知子目录/zip 位置被合并时该行为也有特殊处理见下文MultiplexedPath。TraversableResources面向files()的新资源读取接口TraversableResources是现役推荐的资源读取 ABC其与ResourceReader的关系非常简洁它继承ResourceReader它为ResourceReader的全部抽象方法提供了基于files()的具体实现因此任何提供了TraversableResources的 loader 也必然等价于提供了ResourceReader超集关系新接口只需要实现一个抽象方法files()返回加载的包对应的Traversable对象。ResourceReader四个方法在 Lib/importlib/resources/abc.py 中的默认实现为class TraversableResources(ResourceReader): abc.abstractmethod def files(self) - Traversable: ... def open_resource(self, resource: StrPath) - BinaryIO: return self.files().joinpath(resource).open(rb) def resource_path(self, resource: Any) - NoReturn: raise FileNotFoundError(resource) def is_resource(self, path: StrPath) - bool: return self.files().joinpath(path).is_file() def contents(self) - Iterator[str]: return (item.name for item in self.files().iterdir())逐条对读即可发现设计意图open_resource→files().joinpath(resource).open(rb)把“扁平文件名”转换为Traversable树中的路径再以二进制打开is_resource→joinpath(path).is_file()文件即资源contents→ 由files().iterdir()各子项的name组成生成器resource_path→无条件抛FileNotFoundError因为Traversable未必真实存在于文件系统例如 zip 内部无法给出可靠的本地路径——这是与FileReader这类“文件系统原生”实现的关键差异后者会重写resource_path返回真实路径避免上层as_file做临时拷贝。CPython 内置 loader 的标准实现标准库已在真实场景中把上述接口“跑通”对应三个典型 loader均在各自的get_resource_reader()中返回读者实现类Loaderget_resource_reader()返回源码位置FileLoader/SourceFileLoader/SourcelessFileLoader文件系统FileReaderLib/importlib/_bootstrap_external.pyzipimporterzip 归档ZipReaderLib/zipimport.pyNamespaceLoader命名空间包NamespaceReaderLib/importlib/_bootstrap_external.py这些 reader 的实现全部位于 Lib/importlib/resources/readers.py其类关系清晰对应上面的 ABC 设计FileReader文件系统的“零拷贝”捷径class FileReader(abc.TraversableResources): def __init__(self, loader): self.path pathlib.Path(loader.path).parent def resource_path(self, resource): # 返回真实文件系统路径 # 从而避免 resources.path() 创建临时副本。 return str(self.path.joinpath(resource)) def files(self): return self.pathfiles()直接返回pathlib.Path——因为pathlib.Path天然实现了Traversable所需子集。特别地它重写了resource_path既然文件本来就在磁盘上直接给出路径即可让上层resources.path()免去_tempfile()的临时文件开销注释与实现对应可见 Lib/importlib/resources/readers.py。ZipReader处理 zip 内路径前缀与边界情形class ZipReader(abc.TraversableResources): def __init__(self, loader, module): self.prefix loader.prefix.replace(\\, /) if loader.is_package(module): _, _, name module.rpartition(.) self.prefix name / self.archive loader.archiveZipReader需要把 loader 的prefix与包名拼接成 zip 内部目录前缀并用zipfile.Path(self.archive, self.prefix)作为files()的返回值。它另外做了两处健壮性修正Lib/importlib/resources/readers.pyopen_resource捕获底层KeyError并转译为FileNotFoundErroris_resource增加target.exists()判断规避zipfile.Path.is_file()对不存在路径仍返回True的历史怪癖。NamespaceReader 与 MultiplexedPath多宿命名空间包命名空间包可同时“散落”在多个目录甚至 zip 中因此NamespaceReader用MultiplexedPath把多个Traversable合并为一个对外一致的目录视图它以name排序并分组各子路径的iterdir()结果同名条目再决定是返回单个对象、还是再次合并为MultiplexedPath见 Lib/importlib/resources/readers.pyMultiplexedPath.joinpath在TraversalError时会“降级”返回第一个子路径的拼接结果保证调用方拿到的是一个确定不存在的对象而非崩溃其_resolve_zip_path还会对形如/foo/baz.zip/inner_dir的路径反向尝试解析出 zip 内部的zipfile.Path说明一个Traversable完全可以指向 zip 内部节点。对不支持files()的旧 loader 的适配为了让“只实现ResourceReader旧接口”的 loader 也能被files()使用CPython 在 Lib/importlib/resources/_adapters.py 提供TraversableResourcesLoader它的get_resource_reader()通过CompatibilityFiles把旧 reader 包装成具备files()能力的对象SpecPath/ChildPath/OrphanPath三种路径分别代表“包根/资源子项/悬空路径”。这是理解“为什么只要实现ResourceReader的 loader 在 3.11 上依然可用”的关键。上层调用链files()与as_file如何依赖这些 ABCimportlib.resources.files()API 文档见 Doc/library/importlib.resources.rst是普通用户每天打交道最多的函数其内部正好贯穿了上述所有抽象调用链如下实现见 Lib/importlib/resources/_common.pyfiles(anchorNone)通过resolve()解析包对象字符串会被import_moduleNone则从调用栈推断调用者所在模块from_package(package)先做_assert_spec校验对__main__且__spec__ is None的情况给出清晰报错再经wrap_spec()适配后调用spec.loader.get_resource_reader(spec.name)最终返回reader.files()即一个Traversable。而as_file(path)则解决了“拿到真实文件路径”的需求Lib/importlib/resources/_common.py若传入的path本身就是pathlib.Path直接yield原对象“退化行为”零拷贝若它是存在于文件系统的Traversable目录则递归把整棵目录树复制进临时目录_write_contents其余情况如 zip 内部则把path.read_bytes()写到tempfile.mkstemp()创建的临时文件再yield并保证上下文退出时清理。顺带一提FileReader/NamespaceReader重写resource_path的注释之所以反复强调“避免临时副本”正是因为 as_file 的内部实现会走这条_tempfile路径。实战如何为自定义包加载器接入资源读取要在自己的 loader 上支持importlib.resources.files()只需两步实现一个TraversableResources子类再让 loader 的get_resource_reader()返回它。下面是一个最小可运行示例结构与标准测试 Lib/test/test_importlib/resources/test_custom.py 中的做法一致from importlib.resources import abc class MyPackageReader(abc.TraversableResources): 把某个磁盘目录视作包目录的资源读取器。 def __init__(self, directory): self.directory directory def files(self): # pathlib.Path 已实现 Traversable 协议所需的方法子集 # 因此可以直接返回它。 return self.directory class MyLoader: 极简 loader只负责提供资源读取器。 def __init__(self, directory): self.directory directory def get_resource_reader(self, fullname): # 非包返回 None这里假设 fullname 一定是包。 return MyPackageReader(self.directory)接入后即可统一使用高层 APIimport importlib.resources as resources files resources.files(mypkg) # - MyPackageReader.files() 返回的 Traversable data files.joinpath(data, table.dat).read_bytes() # 等价 files/data/table.dat text (files / README.md).read_text(encodingutf-8)测试端亦可复用仓库现有用例思路Lib/test/test_importlib/resources/test_custom.py中的SimpleLoader把一个ResourceReader实例直接塞给get_resource_reader而MagicResources(TraversableResources)子类仅靠files()返回self.path便完成了整个接口。仓库还提供了面向“极简低层 reader”的桥接实现 Lib/importlib/resources/simple.py其SimpleReader只要求package/children/resources/open_binary四个成员而TraversableReader(TraversableResources, SimpleReader)自动把SimpleReader适配成TraversableResources——如果你的资源来自远程、压缩流或自定义容器这是最省力的接入模板。需要补充的实现细节同样有源码与测试佐证files()返回的Traversable的open(moder)会透传io.TextIOWrapper参数文本读取务必显式给出encodingiterdir()产出的子项不保证全是“资源”调用is_resource()/is_file()前请容忍目录等非资源项当路径无法被遍历到时默认joinpath会抛TraversalErrorabc内定义上层捕获它的典型场景是MultiplexedPath从 3.11 起Traversable是runtime_checkable的Protocol可用isinstance(x, abc.Traversable)安全探测。版本演进与迁移建议版本变化Python 3.10ResourceReader.is_resource()的参数由name更名为path历史变更记录于该模块文档的versionchanged条目Python 3.11新增importlib.resources.abc模块Traversable.joinpath()支持多段参数与/分隔importlib.resources.files()成为主力 APIPython 3.12弃用ResourceReader统一改用TraversableResources迁移建议非常直接如果实现的是 loader让get_resource_reader()返回TraversableResources或直接返回实现了Traversable的pathlib.Path/zipfile.Path不要再手工实现ResourceReader的四个旧方法如果调用方遇到DeprecationWarning检查是否在依赖老式ResourceReader接口改用files()/as_file高层 API 即可兼容期行为由 _adapters.py 与 readers.py 中的包装层兜底老 loader 仍可通过CompatibilityFiles工作但旧代码终将随废弃周期结束而移除。小结ResourceReader是资源读取能力的“旧约”扁平文件名、四个抽象方法、get_resource_reader(fullname)契约3.12 起废弃Traversable是“pathlib.Path 子集”提供name/iterdir()/is_dir()/is_file()/open()并免费获得read_bytes()/read_text()/joinpath()/__truediv__TraversableResources是“新约”唯一抽象方法files()其余方法全部基于files()具体化是 3.12 后所有 loader 应实现的接口CPython 内置的FileReader、ZipReader、NamespaceReader/MultiplexedPath及_adapters.py适配层共同印证了三层接口在“文件系统、zip、命名空间包”三种真实存储形态下的通用性对普通开发者而言最终体验收敛为一行代码files(mypkg).joinpath(...).read_text(...)——其背后正是这套 ABC 在支撑。如需继续深入建议按顺序阅读官方文档 Doc/library/importlib.resources.abc.rst 与配套的 Doc/library/importlib.resources.rst再对照核心实现 Lib/importlib/resources/abc.py、Lib/importlib/resources/readers.py 与测试用例 Lib/test/test_importlib/resources/ 逐行印证。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考