5.5. 基于路径的查找器¶
如前所述,Python 自带几个默认的元路径查找器。其中一个,称为基于路径的查找器(PathFinder),会搜索一个导入路径,该路径包含一个路径条目列表。每个路径条目都指定了一个搜索模块的位置。
基于路径的查找器本身不知道如何导入任何东西。相反,它会遍历各个路径条目,将每个条目与一个知道如何处理该特定类型路径的路径条目查找器关联起来。
默认的路径条目查找器集合实现了在文件系统上查找模块的所有语义,处理特殊的文件类型,如 Python 源代码(.py 文件)、Python 字节码(.pyc 文件)和共享库(例如 .so 文件)。当标准库中的 zipimport 模块支持时,默认的路径条目查找器还处理从 zip 文件加载所有这些文件类型(共享库除外)。
路径条目不必局限于文件系统位置。它们可以指向 URL、数据库查询,或任何可以用字符串指定的其他位置。
基于路径的查找器提供了额外的钩子和协议,以便您可以扩展和自定义可搜索路径条目的类型。例如,如果您想支持将网络 URL 作为路径条目,您可以编写一个实现 HTTP 语义的钩子,用于在 Web 上查找模块。这个钩子(一个可调用对象)将返回一个支持下述协议的路径条目查找器,然后用它从 Web 获取模块的加载器。
警告:本节和前一节都使用了术语 *finder*,通过使用元路径查找器和路径条目查找器来区分它们。这两种类型的查找器非常相似,支持相似的协议,并在导入过程中以相似的方式工作,但重要的是要记住它们有细微的差别。特别是,元路径查找器在导入过程的开始阶段操作,以 sys.meta_path 遍历为触发点。
相比之下,路径条目查找器在某种意义上是基于路径的查找器的实现细节,事实上,如果将基于路径的查找器从 sys.meta_path 中移除,任何路径条目查找器的语义都不会被调用。
5.5.1. 路径条目查找器¶
基于路径的查找器负责查找和加载那些位置由字符串路径条目指定的 Python 模块和包。大多数路径条目指定了文件系统中的位置,但它们不必局限于此。
作为元路径查找器,基于路径的查找器实现了之前描述的 find_spec() 协议,但它暴露了额外的钩子,可用于自定义如何从导入路径中查找和加载模块。
基于路径的查找器使用三个变量:sys.path、sys.path_hooks 和 sys.path_importer_cache。包对象上的 __path__ 属性也被使用。这些提供了自定义导入机制的额外方式。
sys.path 包含一个字符串列表,提供模块和包的搜索位置。它从 PYTHONPATH 环境变量以及各种其他安装和实现相关的默认值初始化。sys.path 中的条目可以命名文件系统上的目录、zip 文件,以及可能应该搜索模块的其他“位置”(参见 site 模块),例如 URL 或数据库查询。sys.path 中只应出现字符串;所有其他数据类型都将被忽略。
基于路径的查找器是一个元路径查找器,因此导入机制通过调用基于路径的查找器的 find_spec() 方法来开始导入路径搜索,如前所述。当向 find_spec() 传入 path 参数时,它将是一个要遍历的字符串路径列表——通常是包内导入时该包的 __path__ 属性。如果 path 参数为 None,这表示一个顶层导入,将使用 sys.path。
基于路径的查找器会遍历搜索路径中的每个条目,并为每个条目寻找一个合适的路径条目查找器(PathEntryFinder)。因为这可能是一个昂贵的操作(例如,此搜索可能涉及 stat() 调用的开销),基于路径的查找器会维护一个将路径条目映射到路径条目查找器的缓存。这个缓存维护在 sys.path_importer_cache 中(尽管名称如此,这个缓存实际上存储的是查找器对象,而不仅仅限于导入器对象)。这样,对特定路径条目位置的路径条目查找器的昂贵搜索只需执行一次。用户代码可以自由地从 sys.path_importer_cache 中移除缓存条目,从而强制基于路径的查找器再次执行路径条目搜索。
如果路径条目不在缓存中,基于路径的查找器会遍历 sys.path_hooks 中的每个可调用对象。此列表中的每个路径条目钩子都会被调用,并传入一个参数,即要搜索的路径条目。这个可调用对象可以返回一个能够处理该路径条目的路径条目查找器,也可以引发 ImportError。基于路径的查找器使用 ImportError 来表示钩子无法为该路径条目找到路径条目查找器。该异常会被忽略,导入路径的迭代会继续。钩子应期望接收一个字符串或字节对象;字节对象的编码由钩子决定(例如,它可能是文件系统编码、UTF-8 或其他),如果钩子无法解码该参数,它应该引发 ImportError。
如果 sys.path_hooks 的迭代结束时没有返回任何 路径入口查找器,那么基于路径的查找器的 find_spec() 方法将在 sys.path_importer_cache 中存入 None(表示该路径入口没有查找器),并返回 None,表明此 元路径查找器 无法找到该模块。
如果 sys.path_hooks 上的某个 路径入口钩子 可调用对象确实返回了一个 路径入口查找器,则会使用以下协议来请求查找器提供一个模块 spec,这个 spec 之后会用于加载模块。
当前工作目录(由一个空字符串表示)的处理方式与 sys.path 上的其他条目略有不同。首先,如果无法确定当前工作目录或发现其不存在,则不会在 sys.path_importer_cache 中存储任何值。其次,每次查找模块时,都会重新查找当前工作目录的值。第三,用于 sys.path_importer_cache 和由 importlib.machinery.PathFinder.find_spec() 返回的路径将是实际的当前工作目录,而不是空字符串。
5.5.2. 路径入口查找器协议¶
为了支持导入模块和已初始化的包,并为命名空间包贡献部分内容,路径入口查找器必须实现 find_spec() 方法。
find_spec() 接受两个参数:要导入的模块的完全限定名称,以及(可选的)目标模块。find_spec() 返回一个该模块的完整 spec。这个 spec 的 "loader" 属性总是会被设置(有一个例外)。
为了向导入机制表明 spec 代表一个命名空间 部分,路径入口查找器需要将 submodule_search_locations 设置为一个包含该部分的列表。
在 3.4 版更改: find_spec() 取代了 find_loader() 和 find_module(),这两个方法现已弃用,但如果 find_spec() 未定义,仍会使用它们。
较旧的路径入口查找器可能会实现这两个已弃用的方法之一,而不是 find_spec()。为了向后兼容,这些方法仍然受到支持。但是,如果在路径入口查找器上实现了 find_spec(),则会忽略这些遗留方法。
find_loader() 接受一个参数,即要导入的模块的完全限定名称。find_loader() 返回一个二元组,其中第一项是加载器,第二项是命名空间 部分。
为了与导入协议的其他实现向后兼容,许多路径入口查找器也支持元路径查找器所支持的、传统的 find_module() 方法。然而,路径入口查找器的 find_module() 方法在调用时从不带 path 参数(它们应从对路径钩子的初始调用中记录适当的路径信息)。
路径入口查找器上的 find_module() 方法已被弃用,因为它不允许路径入口查找器为命名空间包贡献部分内容。如果路径入口查找器上同时存在 find_loader() 和 find_module(),导入系统将总是优先调用 find_loader()。
在 3.10 版更改: 导入系统调用 find_module() 和 find_loader() 将会引发 ImportWarning。
在 3.12 版更改: find_module() 和 find_loader() 已被移除。