5.5. 基于路径的查找器

在之前已经提及,Python 带有几种默认的元路径查找器。 其中之一是 path based finder (PathFinder),它会搜索包含一个 路径条目 列表的 import path。 每个路径条目指定一个用于搜索模块的位置。

基于路径的查找器自身并不知道如何进行导入。 它只是遍历单独的路径条目,将它们各自关联到某个知道如何处理特定类型路径的路径条目查找器。

默认的路径条目查找器集合实现了在文件系统中查找模块的所有语义,可处理多种特殊文件类型例如 Python 源码 (.py 文件),Python 字节码 (.pyc 文件) 以及共享库 (例如 .so 文件)。 在标准库中 zipimport 模块的支持下,默认路径条目查找器还能处理所有来自 zip 文件的上述文件类型。

路径条目不必仅限于文件系统位置。 它们可以指向 URL、数据库查询或可以用字符串指定的任何其他位置。

基于路径的查找器还提供了额外的钩子和协议以便能扩展和定制可搜索路径条目的类型。 例如,如果你想要支持网络 URL 形式的路径条目,你可以编写一个实现 HTTP 语义在网络上查找模块的钩子。 这个钩子(可调用对象)应当返回一个支持下述协议的 path entry finder,以被用来获取一个专门针对来自网络的模块的加载器。

预先的警告:本节和上节都使用了 查找器 这一术语,并通过 meta path finderpath entry finder 两个术语来明确区分它们。 这两种类型的查找器非常相似,支持相似的协议,且在导入过程中以相似的方式运作,但关键的一点是要记住它们是有微妙差异的。 特别地,元路径查找器作用于导入过程的开始,主要是启动 sys.meta_path 遍历。

相比之下,路径条目查找器在某种意义上说是基于路径的查找器的实现细节,实际上,如果需要从 sys.meta_path 移除基于路径的查找器,并不会有任何路径条目查找器被发起调用。

5.5.1. 路径条目查找器

path based finder 会负责查找和加载通过 path entry 字符串来指定位置的 Python 模块和包。 多数路径条目所指定的是文件系统中的位置,但它们并不必受限于此。

作为一种元路径查找器,path based finder 实现了上文描述的 find_spec() 协议,但是它还对外公开了一些附加钩子,可被用来定制模块如何从 import path 查找和加载。

有三个变量由 path based finder, sys.path, sys.path_hookssys.path_importer_cache 所使用。 包对象的 path 属性也会被使用。 它们提供了可用于定制导入机制的额外方式。

sys.path 包含一个提供模块和包搜索位置的字符串列表。 它初始化自 PYTHONPATH 环境变量以及多种其他特定安装和实现的默认设置。 sys.path 条目可指定的名称有文件系统中的目录、zip 文件和其他可用于搜索模块的潜在“位置”(参见 site 模块),例如 URL 或数据库查询等。 在 sys.path 中只能出现字符串和字节串;所有其他数据类型都会被忽略。 字节串条目使用的编码由单独的 路径条目查找器 来确定。

path based finder 是一种 meta path finder,因此导入机制会通过调用上文描述的基于路径的查找器的 find_spec() 方法来启动 import path 搜索。 当要向 find_spec() 传入 path 参数时,它将是一个可遍历的字符串列表 —— 通常为用来在其内部进行导入的包的 path 属性。 如果 path 参数为 None,这表示最高层级的导入,将会使用 sys.path

基于路径的查找器会迭代搜索路径中的每个条目,并且每次都查找与路径条目对应的 path entry finder (PathEntryFinder)。 因为这种操作可能很耗费资源(例如搜索会有 stat() 调用的开销),基于路径的查找器会维持一个缓存来将路径条目映射到路径条目查找器。 这个缓存放于 sys.path_importer_cache (尽管如此命名,但这个缓存实际存放的是查找器对象而非仅限于 importer 对象)。 通过这种方式,对特定 path entry 位置的 path entry finder 的高耗费搜索只需进行一次。 用户代码可以自由地从 sys.path_importer_cache 移除缓存条目,以强制基于路径的查找器再次执行路径条目搜索 3

如果路径条目不存在于缓存中,基于路径的查找器会迭代 sys.path_hooks 中的每个可调用对象。 对此列表中的每个 路径条目钩子 的调用会带有一个参数,即要搜索的路径条目。 每个可调用对象或是返回可处理路径条目的 path entry finder,或是引发 ImportError。 基于路径的查找器使用 ImportError 来表示钩子无法找到与 path entry 相对应的 path entry finder。 该异常会被忽略并继续进行 import path 的迭代。 每个钩子应该期待接收一个字符串或字节串对象;字节串对象的编码由钩子决定(例如可以是文件系统使用的编码 UTF-8 或其它编码),如果钩子无法解码参数,它应该引发 ImportError

如果 sys.path_hooks 迭代结束时没有返回 path entry finder,则基于路径的查找器 find_spec() 方法将在 sys.path_importer_cache 中存入 None (表示此路径条目没有对应的查找器) 并返回 None,表示此 meta path finder 无法找到该模块。

如果 sys.path_hooks 中的某个 path entry hook 可调用对象的返回值 一个 path entry finder,则以下协议会被用来向查找器请求一个模块的规格说明,并在加载该模块时被使用。

当前工作目录 — 由一个空字符串表示 — 的处理方式与 sys.path 中的其他条目略有不同。 首先,如果发现当前工作目录不存在,则 sys.path_importer_cache 中不会存放任何值。 其次,每个模块查找会对当前工作目录的值进行全新查找。 第三,由 sys.path_importer_cache 所使用并由 importlib.machinery.PathFinder.find_spec() 所返回的路径将是实际的当前工作目录而非空字符串。

5.5.2. 路径条目查找器协议

为了支持模块和已初始化包的导入,也为了给命名空间包提供组成部分,路径条目查找器必须实现 find_spec() 方法。

find_spec() 接受两个参数,即要导入模块的完整限定名称,以及(可选的)目标模块。 find_spec() 返回模块的完全填充好的规格说明。 这个规格说明总是包含“加载器”集合(但有一个例外)。

为了向导入机制提示该规格说明代表一个命名空间的 portion。 路径条目查找器会将规格说明中的 "loader" 设为 None 并将 "submodule_search_locations" 设为一个包含该部分的列表。

在 3.4 版更改: find_spec() 替代了 find_loader()find_module(),后两者现在都已弃用,但会在 find_spec() 未定义时被使用。

较旧的路径条目查找器可能会实现这两个已弃用的方法中的一个而没有实现 find_spec()。 为保持向后兼容,这两个方法仍会被接受。 但是,如果在路径条目查找器上实现了 find_spec(),这两个遗留方法就会被忽略。

find_loader() 接受一个参数,即被导入模块的完整限定名称。 find_loader() 会返回一个二元组,其中第一项为加载器,第二项为一个命名空间 portion。 当第一项(即加载器)为 None 时,这意味着路径条目查找器虽然没有指定名称模块的加载器,但它知道该路径条目为指定名称模块提供了一个命名空间部分。 这几乎总是表明一种情况,即 Python 被要求导入一个并不以文件系统中的实体形式存在的命名空间包。 当一个路径条目查找器返回的加载器为 None 时,该二元组返回值的第二项必须为一个序列,不过它也可以为空。

如果 find_loader() 所返回加载器的值不为 None,则该部分会被忽略,而该加载器会自基于路径的查找器返回,终止对路径条目的搜索。

为了向后兼容其他导入协议的实现,许多路径条目查找器也同样支持元路径查找器所支持的传统 find_module() 方法。 但是路径条目查找器 find_module() 方法的调用绝不会带有 path 参数(它们被期望记录来自对路径钩子初始调用的恰当路径信息)。

路径条目查找器的 find_module() 方法已弃用,因为它不允许路径条目查找器为命名空间包提供部分。 如果 find_loader()find_module() 同时存在于一个路径条目查找器中,导入系统将总是调用 find_loader() 而不选择 find_module()