Python中导入模块的完整限定名称的概念,这对于理解Python模块导入的机制非常重要。在Python中,需要使用模块的完整限定名称来进行导入,这个名称可以是通过import语句指定的参数,也可以是传递给importlib.import_module()或import()函数的参数。
这个完整限定名称会在导入搜索的各个阶段被使用,如果是指向子模块的路径,例如foo.bar.baz,Python会按顺序尝试导入foo、foo.bar和foo.bar.baz这几个模块。如果其中任何一个导入失败,就会触发ModuleNotFoundError异常。
一、模块缓存
在导入搜索期间首先会被检查的地方是 sys.modules, 这个映射起到缓存之前导入的所有模块的作用(包括其中间路径), 因此如果之前导入过 foo.bar.baz,则 sys.modules 将包含 foo, foo.bar 和 foo.bar.baz 条目, 每个键的值就是相应的模块对象。
在导入期间,会在 sys.modules 查找模块名称,如存在则其关联的值就是需要导入的模块,导入过程完成。 然而,如果值为 None,则会引发 ModuleNotFoundError; 如果找不到指定模块名称,Python 将继续搜索该模块。
sys.modules 是可写的。删除键可能不会破坏关联的模块(因为其他模块可能会保留对它的引用),但它会使命名模块的缓存条目无效,导致 Python 在下次导入时重新搜索命名模块。键也可以赋值为 None ,强制下一次导入模块导致 ModuleNotFoundError 。
注意:如果还保有对某个模块对象的引用,同时停用其在 sys.modules 中的缓存条目,然后又再次导入该名称的模块,则前后两个模块对象将不是同一个。 相反地,importlib.reload() 将重用 同一个 模块对象,并简单地通过重新运行模块的代码来重新初始化模块内容。
二、查找器和加载器
如果指定名称的模块在 sys.modules 找不到,则将发起调用 Python 的导入协议以查找和加载该模块。 此协议由两个概念性模块构成,即查找器和加载器。 查找器的任务是确定是否能使用其所知的策略找到该名称的模块。 同时实现这两种接口的对象称为导入器 —— 它们在确定能加载所需的模块时会返回其自身。
Python 包含了多个默认查找器和导入器。 第一个知道如何定位内置模块;第二个知道如何定位冻结模块;第三个默认查找器会在 import path 中搜索模块。 import path 是一个由文件系统路径或 zip 文件组成的位置列表, 它还可以扩展为搜索任意可定位资源,例如由 URL 指定的资源。导入机制是可扩展的,因此可以加入新的查找器以扩展模块搜索的范围和作用域。
查找器并不真正加载模块, 如果它们能找到指定名称的模块,会返回一个 模块规格说明,这是对模块导入相关信息的封装,供后续导入机制用于在加载模块时使用。
在 3.4 版更改:在之前的 Python 版本中,查找器会直接返回加载器,现在它们则返回模块规格说明,其中包含加载器。 加载器仍然在导入期间被使用,但负担的任务有所减少。
三、导入钩子
导入机制的可扩展性是Python设计的一个重要特点,而实现这一特点的基本机制就是导入钩子。导入钩子分为两种类型:元钩子和导入路径钩子。
- 元钩子在导入过程开始时被调用,此时除了sys.modules缓存查找之外,还没有进行任何其他的导入过程。这使得元钩子可以重载sys.path的过程,甚至可以冻结模块或者内置模块。要注册元钩子,需要向sys.meta_path添加新的查找器对象来实现。
- 导入路径钩子则是作为sys.path(或package.path)过程的一部分,在遇到它们所关联的路径项时被调用。要注册导入路径钩子,则需要向sys.path_hooks添加新的可调用对象。
这种灵活的导入机制设计使得Python可以方便地进行模块的定制和扩展,能够满足各种复杂场景下对导入过程的定制需求。
四、元路径
当指定名称的模块在 sys.modules 中找不到时,Python 会接着搜索 sys.meta_path,其中包含元路径查找器对象列表,这些查找器按顺序被查询以确定它们是否知道如何处理该名称的模块。 元路径查找器必须实现名为 find_spec() 的方法,该方法接受三个参数:名称、导入路径和目标模块(可选)。 元路径查找器可使用任何策略来确定它是否能处理指定名称的模块。
如果元路径查找器知道如何处理指定名称的模块,将返回一个说明对象; 如果它不能处理该名称的模块,则会返回 None; 如果 sys.meta_path 处理过程到达列表末尾仍未返回说明对象,则将引发 ModuleNotFoundError。 任何其他被引发异常将直接向上传播,并放弃导入过程。
元路径查找器的 find_spec() 方法调用带有两到三个参数。 第一个是被导入模块的完整限定名称,例如 foo.bar.baz;第二个参数是供模块搜索使用的路径条目。 对于最高层级模块,第二个参数为 None,但对于子模块或子包,第二个参数为父包 __path__ 属性的值。 如果相应的 __path__ 属性无法访问,将引发 ModuleNotFoundError;第三个参数是一个将被作为稍后加载目标的现有模块对象。 导入系统仅会在重加载期间传入一个目标模块。
对于单个导入请求可以多次遍历元路径。 例如,假设所涉及的模块都尚未被缓存,则导入 foo.bar.baz 将首先执行顶级的导入,在每个元路径查找器 (mpf) 上调用 mpf.find_spec(“foo”, None, None)。 在导入 foo 之后,foo.bar 将通过第二次遍历元路径来导入,调用 mpf.find_spec(“foo.bar”, foo.__path__, None)。 一旦 foo.bar 完成导入,最后一次遍历将调用 mpf.find_spec(“foo.bar.baz”, foo.bar.__path__, None)。
有些元路径查找器只支持顶级导入, 当把 None 以外的对象作为第三个参数传入时,这些导入器将总是返回 None。
Python 的默认 sys.meta_path 具有三种元路径查找器,一种知道如何导入内置模块,一种知道如何导入冻结模块,还有一种知道如何导入来自 import path 的模块 (即 path based finder)。
- 在 3.4 版更改: 元路径查找器的 find_spec() 方法替代了 find_module(),后者现已被弃用。 虽然它将继续可用但不会再做改变,导入机制仅会在查找器未实现 find_spec() 时尝试使用它。
- 在 3.10 版更改: 导入系统使用 find_module() 现在将引发 ImportWarning。
- 在 3.12 版更改: find_module() 已被移除。 请改用 find_spec()。