5. 导入系统

一个 模块 中的 Python 代码通过 导入 另一个模块的过程来访问另一个模块中的代码。import 语句是调用导入机制的最常见方式,但它不是唯一的方式。 诸如 importlib.import_module() 和内置的 __import__() 等函数也可用于调用导入机制。

import 语句结合了两个操作;它搜索指定的模块,然后将搜索结果绑定到局部作用域中的名称。 import 语句的搜索操作定义为调用 __import__() 函数,并带有适当的参数。__import__() 的返回值用于执行 import 语句的名称绑定操作。 有关该名称绑定操作的确切细节,请参阅 import 语句。

直接调用 __import__() 仅执行模块搜索,如果找到,则执行模块创建操作。 虽然可能会发生某些副作用,例如导入父包以及更新各种缓存(包括 sys.modules),但只有 import 语句执行名称绑定操作。

当执行 import 语句时,将调用标准的内置 __import__() 函数。 调用导入系统的其他机制(例如 importlib.import_module())可能会选择绕过 __import__() 并使用自己的解决方案来实现导入语义。

当第一次导入模块时,Python 会搜索该模块,如果找到,则创建一个模块对象[1],并初始化它。 如果找不到指定的模块,则会引发 ModuleNotFoundError。当调用导入机制时,Python 会实现各种策略来搜索指定的模块。 可以使用下面各节中描述的各种钩子来修改和扩展这些策略。

在 3.3 版本中更改: 导入系统已更新为完全实现 PEP 302 的第二阶段。 不再有任何隐式导入机制 - 完整的导入系统通过 sys.meta_path 公开。 此外,还实现了原生命名空间包支持(参见 PEP 420)。

5.1. importlib

importlib 模块提供了一个丰富的 API,用于与导入系统交互。 例如,importlib.import_module() 提供了一个推荐的、比内置的 __import__() 更简单的 API 来调用导入机制。 有关更多详细信息,请参阅 importlib 库文档。

5.2.

Python 只有一种模块对象类型,所有模块都属于此类型,无论模块是在 Python、C 还是其他语言中实现的。 为了帮助组织模块并提供命名层次结构,Python 具有 的概念。

可以将包视为文件系统上的目录,将模块视为目录中的文件,但不要过于字面地理解这个类比,因为包和模块不必源自文件系统。 为了本文档的目的,我们将使用这种方便的目录和文件的类比。 就像文件系统目录一样,包是按层次结构组织的,并且包本身可能包含子包以及常规模块。

请务必记住,所有包都是模块,但并非所有模块都是包。 换句话说,包只是一种特殊的模块。 具体来说,任何包含 __path__ 属性的模块都被视为包。

所有模块都有一个名称。 子包名称与其父包名称用点分隔,类似于 Python 的标准属性访问语法。 因此,您可能会有一个名为 email 的包,它又有一个名为 email.mime 的子包,以及该子包中的一个名为 email.mime.text 的模块。

5.2.1. 常规包

Python 定义了两种类型的包:常规包命名空间包。 常规包是 Python 3.2 及更早版本中存在的传统包。 常规包通常实现为包含 __init__.py 文件的目录。 当导入常规包时,将隐式执行此 __init__.py 文件,并且它定义的对象将绑定到包命名空间中的名称。 __init__.py 文件可以包含任何其他模块可以包含的相同 Python 代码,并且 Python 将在导入时向模块添加一些额外的属性。

例如,以下文件系统布局定义了一个具有三个子包的顶级 parent

parent/
    __init__.py
    one/
        __init__.py
    two/
        __init__.py
    three/
        __init__.py

导入 parent.one 将隐式执行 parent/__init__.pyparent/one/__init__.py。 后续导入 parent.twoparent.three 将分别执行 parent/two/__init__.pyparent/three/__init__.py

5.2.2. 命名空间包

命名空间包是各种部分的组合,其中每个部分为父包贡献一个子包。这些部分可能位于文件系统上的不同位置。部分也可能存在于 zip 文件中、网络上或 Python 在导入时搜索的任何其他位置。命名空间包可能直接对应于文件系统上的对象,也可能是不具有具体表示形式的虚拟模块。

命名空间包不使用普通的列表作为其 __path__ 属性。它们而是使用自定义的可迭代类型,如果在父包的路径(或者对于顶层包,使用 sys.path)发生变化,该类型将在该包的下一次导入尝试时自动执行新的包部分搜索。

使用命名空间包时,不存在 parent/__init__.py 文件。实际上,在导入搜索期间可能会找到多个 parent 目录,其中每个目录由不同的部分提供。因此,parent/one 可能在物理上并不与 parent/two 相邻。在这种情况下,每当导入顶层 parent 包或其子包之一时,Python 都会为该顶层 parent 包创建一个命名空间包。

另请参阅 PEP 420,了解命名空间包规范。

5.3. 搜索

要开始搜索,Python 需要导入模块(或包,但为了本文讨论的目的,差异无关紧要)的完全限定名称。此名称可能来自 import 语句的各种参数,或者来自 importlib.import_module()__import__() 函数的参数。

此名称将用于导入搜索的各个阶段,并且它可能是子模块的点分隔路径,例如 foo.bar.baz。在这种情况下,Python 首先尝试导入 foo,然后导入 foo.bar,最后导入 foo.bar.baz。如果任何中间导入失败,则会引发 ModuleNotFoundError

5.3.1. 模块缓存

导入搜索期间检查的第一个位置是 sys.modules。此映射用作以前导入的所有模块(包括中间路径)的缓存。因此,如果以前导入了 foo.bar.baz,则 sys.modules 将包含 foofoo.barfoo.bar.baz 的条目。每个键的值将是相应的模块对象。

在导入期间,模块名称在 sys.modules 中查找,如果存在,则关联的值是满足导入的模块,并且该过程完成。但是,如果值为 None,则会引发 ModuleNotFoundError。如果模块名称缺失,Python 将继续搜索该模块。

sys.modules 是可写的。删除键可能不会销毁关联的模块(因为其他模块可能持有对它的引用),但它会使命名模块的缓存条目无效,从而导致 Python 在下次导入时重新搜索命名模块。也可以将键赋值为 None,强制下次导入该模块时引发 ModuleNotFoundError

但请注意,如果您保留对模块对象的引用,使其在 sys.modules 中的缓存条目无效,然后重新导入该命名模块,则这两个模块对象将相同。相比之下,importlib.reload() 将重用相同的模块对象,并简单地通过重新运行模块的代码来重新初始化模块内容。

5.3.2. 查找器和加载器

如果在 sys.modules 中找不到命名模块,则会调用 Python 的导入协议来查找和加载模块。此协议由两个概念对象组成,查找器加载器。查找器的工作是使用它所知道的任何策略来确定是否可以找到命名模块。实现这两个接口的对象称为 导入器 - 当它们发现可以加载请求的模块时,它们会返回自身。

Python 包括许多默认查找器和导入器。第一个知道如何定位内置模块,第二个知道如何定位冻结模块。第三个默认查找器会在 导入路径中搜索模块。导入路径是可能命名文件系统路径或 zip 文件的位置列表。它也可以扩展到搜索任何可定位的资源,例如由 URL 标识的资源。

导入机制是可扩展的,因此可以添加新的查找器来扩展模块搜索的范围和范围。

查找器实际上不加载模块。如果它们可以找到命名模块,则它们会返回一个 模块规范,它是模块导入相关信息的封装,导入机制在加载模块时使用该规范。

以下各节将更详细地描述查找器和加载器的协议,包括如何创建和注册新的查找器和加载器以扩展导入机制。

在 3.4 版本中更改:在以前版本的 Python 中,查找器直接返回 加载器,而现在它们返回包含加载器的模块规范。加载器仍在导入期间使用,但职责较少。

5.3.3. 导入钩子

导入机制被设计为可扩展的;实现此目的的主要机制是导入钩子。有两种类型的导入钩子:元钩子导入路径钩子

元钩子在导入处理开始时调用,在发生任何其他导入处理之前,除了 sys.modules 缓存查找之外。这允许元钩子覆盖 sys.path 处理、冻结模块甚至内置模块。通过将新的查找器对象添加到 sys.meta_path 中来注册元钩子,如下所述。

导入路径钩子作为 sys.path(或 package.__path__)处理的一部分调用,在其关联的路径项被遇到时调用。通过将新的可调用对象添加到 sys.path_hooks 中来注册导入路径钩子,如下所述。

5.3.4. 元路径

如果在 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 具有三个元路径查找器,一个知道如何导入内置模块,一个知道如何导入冻结模块,以及一个知道如何从导入路径(即基于路径的查找器)导入模块。

在 3.4 版本中更改: 元路径查找器的 find_spec() 方法取代了 find_module(),后者现在已弃用。虽然它将继续工作而无需更改,但只有当查找器未实现 find_spec() 时,导入机制才会尝试它。

在 3.10 版本中更改: 现在,导入系统使用 find_module() 会引发 ImportWarning

在 3.12 版本中更改: find_module() 已被移除。请改用 find_spec()

5.4. 加载

如果找到模块规范,导入机制将在加载模块时使用它(以及其中包含的加载器)。以下是在导入的加载部分中发生的大致过程:

module = None
if spec.loader is not None and hasattr(spec.loader, 'create_module'):
    # It is assumed 'exec_module' will also be defined on the loader.
    module = spec.loader.create_module(spec)
if module is None:
    module = ModuleType(spec.name)
# The import-related module attributes get set here:
_init_module_attrs(spec, module)

if spec.loader is None:
    # unsupported
    raise ImportError
if spec.origin is None and spec.submodule_search_locations is not None:
    # namespace package
    sys.modules[spec.name] = module
elif not hasattr(spec.loader, 'exec_module'):
    module = spec.loader.load_module(spec.name)
else:
    sys.modules[spec.name] = module
    try:
        spec.loader.exec_module(module)
    except BaseException:
        try:
            del sys.modules[spec.name]
        except KeyError:
            pass
        raise
return sys.modules[spec.name]

请注意以下细节

  • 如果 sys.modules 中存在具有给定名称的现有模块对象,则导入将已返回它。

  • 模块将存在于 sys.modules 中,然后再由加载器执行模块代码。这至关重要,因为模块代码可能会(直接或间接)导入自身;预先将其添加到 sys.modules 可防止在最坏情况下出现无限递归,并在最佳情况下防止多次加载。

  • 如果加载失败,则失败的模块(且仅失败的模块)将从 sys.modules 中删除。已经存在于 sys.modules 缓存中的任何模块,以及作为副作用成功加载的任何模块,都必须保留在缓存中。这与重新加载形成对比,在重新加载中,即使是失败的模块也会留在 sys.modules 中。

  • 在创建模块之后但在执行之前,导入机制会设置与导入相关的模块属性(上面伪代码示例中的“_init_module_attrs”),如后面的章节中所述。

  • 模块执行是加载的关键时刻,此时模块的命名空间将被填充。执行完全委托给加载器,加载器可以决定填充的内容和方式。

  • 在加载期间创建并传递给 exec_module() 的模块可能不是在导入结束时返回的模块 [2]

在 3.4 版本中更改: 导入系统已接管加载器的样板责任。这些责任之前由 importlib.abc.Loader.load_module() 方法执行。

5.4.1. 加载器

模块加载器提供加载的关键功能:模块执行。导入机制调用 importlib.abc.Loader.exec_module() 方法,并带有一个参数,即要执行的模块对象。从 exec_module() 返回的任何值都将被忽略。

加载器必须满足以下要求

  • 如果模块是 Python 模块(而不是内置模块或动态加载的扩展),则加载器应在模块的全局命名空间 (module.__dict__) 中执行模块的代码。

  • 如果加载器无法执行模块,则应引发 ImportError,尽管在 exec_module() 期间引发的任何其他异常都将被传播。

在许多情况下,查找器和加载器可以是同一个对象;在这种情况下,find_spec() 方法只需返回一个加载器设置为 self 的规范。

模块加载器可以选择在加载期间通过实现 create_module() 方法来创建模块对象。它接受一个参数,即模块规范,并返回在加载期间使用的新模块对象。create_module() 不需要设置模块对象上的任何属性。如果该方法返回 None,则导入机制将自行创建新模块。

在 3.4 版本中新增: 加载器的 create_module() 方法。

在 3.4 版本中更改: load_module() 方法被 exec_module() 替换,导入机制承担了所有加载的样板责任。

为了与现有加载器兼容,如果加载器存在 load_module() 方法并且加载器未实现 exec_module(),则导入机制将使用加载器的 load_module() 方法。但是,load_module() 已被弃用,加载器应改为实现 exec_module()

load_module() 方法必须实现上述所有样板加载功能以及执行模块。所有相同的约束都适用,并做了一些额外的澄清

  • 如果 sys.modules 中存在具有给定名称的现有模块对象,则加载器必须使用该现有模块。(否则,importlib.reload() 将无法正常工作。)如果指定的模块在 sys.modules 中不存在,则加载器必须创建一个新的模块对象并将其添加到 sys.modules

  • 在加载器执行模块代码之前,该模块必须存在于 sys.modules 中,以防止无限递归或多次加载。

  • 如果加载失败,加载器必须移除它已插入到 sys.modules 中的任何模块,但它必须移除失败的模块,且仅当加载器本身已显式加载该模块时才移除。

在 3.5 版本中变更: 当定义了 exec_module() 但未定义 create_module() 时,会引发 DeprecationWarning

在 3.6 版本中变更: 当定义了 exec_module() 但未定义 create_module() 时,会引发 ImportError

在 3.10 版本中变更: 使用 load_module() 将引发 ImportWarning

5.4.2. 子模块

当使用任何机制(例如,importlib API,importimport-from 语句,或内置的 __import__())加载子模块时,会在父模块的命名空间中绑定到子模块对象。例如,如果包 spam 有一个子模块 foo,在导入 spam.foo 之后,spam 将具有一个属性 foo,该属性绑定到该子模块。假设您有以下目录结构

spam/
    __init__.py
    foo.py

并且 spam/__init__.py 中有以下行

from .foo import Foo

那么执行以下操作会在 spam 模块中放置 fooFoo 的名称绑定

>>> import spam
>>> spam.foo
<module 'spam.foo' from '/tmp/imports/spam/foo.py'>
>>> spam.Foo
<class 'spam.foo.Foo'>

鉴于 Python 熟悉的名称绑定规则,这可能看起来令人惊讶,但它实际上是导入系统的基本特征。保持不变的是,如果您有 sys.modules['spam']sys.modules['spam.foo'] (正如您在上面的导入之后所拥有的那样),后者必须作为前者的 foo 属性出现。

5.4.3. 模块规范

导入机制在导入期间使用有关每个模块的各种信息,尤其是在加载之前。 大部分信息对于所有模块都是通用的。模块规范的目的是封装每个模块的此导入相关信息。

在导入期间使用规范允许在导入系统组件之间传输状态,例如,在创建模块规范的查找器和执行它的加载器之间。最重要的是,它允许导入机制执行加载的样板操作,而在没有模块规范的情况下,加载器承担该责任。

模块的规范公开为 module.__spec__。适当地设置 __spec__ 同样适用于 解释器启动期间初始化的模块。唯一的例外是 __main__,其中 __spec__某些情况下设置为 None

有关模块规范内容的详细信息,请参阅 ModuleSpec

在 3.4 版本中新增。

5.4.4. 模块上的 __path__ 属性

__path__ 属性应该是一个(可能为空的)字符串的 序列,列举了将找到包的子模块的位置。根据定义,如果一个模块具有 __path__ 属性,则它是一个

包的 __path__ 属性在其子包的导入期间使用。在导入机制中,它的功能与 sys.path 非常相似,即提供在导入期间搜索模块的位置列表。但是,__path__ 通常比 sys.path 受到的限制要多得多。

用于 sys.path 的相同规则也适用于包的 __path__。遍历包的 __path__ 时,会查询 sys.path_hooks(如下所述)。

包的 __init__.py 文件可以设置或更改包的 __path__ 属性,这通常是在 PEP 420 之前实现命名空间包的方式。随着 PEP 420 的采用,命名空间包不再需要提供仅包含 __path__ 操作代码的 __init__.py 文件;导入机制会自动为命名空间包正确设置 __path__

5.4.5. 模块 repr

默认情况下,所有模块都有一个可用的 repr,但是根据上面设置的属性以及模块的规范,您可以更明确地控制模块对象的 repr。

如果模块具有规范(__spec__),则导入机制将尝试从中生成 repr。如果失败或没有规范,导入系统将使用模块上可用的任何信息来制作默认 repr。它将尝试使用 module.__name__module.__file__module.__loader__ 作为 repr 的输入,并为任何缺失的信息提供默认值。

以下是使用的确切规则

  • 如果模块具有 __spec__ 属性,则使用规范中的信息来生成 repr。将咨询 “name”、“loader”、“origin” 和 “has_location” 属性。

  • 如果模块具有 __file__ 属性,则将其用作模块 repr 的一部分。

  • 如果模块没有 __file__,但确实有 __loader__ 且不为 None,则将加载器的 repr 用作模块 repr 的一部分。

  • 否则,只在 repr 中使用模块的 __name__

在 3.12 版本中变更: 自 Python 3.4 起已弃用的 module_repr() 的使用已在 Python 3.12 中删除,并且在解析模块的 repr 期间不再调用。

5.4.6. 缓存字节码失效

在 Python 从 .pyc 文件加载缓存字节码之前,它会检查缓存是否与源 .py 文件保持最新。默认情况下,Python 通过在写入缓存文件时将源文件的上次修改时间戳和大小存储在缓存文件中来实现这一点。在运行时,导入系统然后通过检查缓存文件中存储的元数据与源的元数据来验证缓存文件。

Python 还支持 “基于哈希” 的缓存文件,该文件存储源文件内容的哈希值,而不是其元数据。存在两种基于哈希的 .pyc 文件的变体:已检查的和未检查的。对于已检查的基于哈希的 .pyc 文件,Python 通过对源文件进行哈希处理并将结果哈希值与缓存文件中的哈希值进行比较来验证缓存文件。如果发现已检查的基于哈希的缓存文件无效,Python 会重新生成该文件并写入一个新的已检查的基于哈希的缓存文件。对于未检查的基于哈希的 .pyc 文件,如果该文件存在,Python 只需假定缓存文件有效。可以使用 --check-hash-based-pycs 标志覆盖基于哈希的 .pyc 文件验证行为。

在 3.7 版本中变更: 添加了基于哈希的 .pyc 文件。以前,Python 仅支持基于时间戳的字节码缓存失效。

5.5. 基于路径的查找器

正如前面提到的,Python 自带几个默认的元路径查找器。其中一个称为基于路径的查找器 (PathFinder),它搜索一个导入路径,其中包含路径条目的列表。每个路径条目指定一个用于搜索模块的位置。

基于路径的查找器本身并不知道如何导入任何内容。相反,它会遍历各个路径条目,将每个路径条目与一个路径条目查找器关联起来,该查找器知道如何处理特定类型的路径。

默认的路径条目查找器集合实现了在文件系统上查找模块的所有语义,处理诸如 Python 源代码(.py 文件)、Python 字节码(.pyc 文件)和共享库(例如 .so 文件)等特殊文件类型。当标准库中的 zipimport 模块支持时,默认的路径条目查找器还可以处理从 zip 文件加载所有这些文件类型(共享库除外)。

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

基于路径的查找器提供了额外的钩子和协议,以便您可以扩展和自定义可搜索的路径条目的类型。例如,如果您想支持将路径条目作为网络 URL,您可以编写一个实现 HTTP 语义的钩子,以在 Web 上查找模块。这个钩子(一个可调用对象)将返回一个路径条目查找器,它支持下面描述的协议,然后该查找器用于从 Web 获取模块的加载器。

一个警告:本节和上一节都使用了术语查找器,并通过使用术语元路径查找器路径条目查找器来区分它们。这两种类型的查找器非常相似,支持类似的协议,并在导入过程中以类似的方式运作,但重要的是要记住它们之间存在细微的差异。特别是,元路径查找器在导入过程的开始阶段运行,通过 sys.meta_path 遍历来触发。

相比之下,路径条目查找器在某种意义上是基于路径的查找器的实现细节,事实上,如果从 sys.meta_path 中删除基于路径的查找器,则不会调用任何路径条目查找器语义。

5.5.1. 路径条目查找器

基于路径的查找器负责查找和加载 Python 模块和包,这些模块和包的位置由字符串路径条目指定。大多数路径条目指定文件系统中的位置,但它们不必仅限于此。

作为元路径查找器,基于路径的查找器实现了先前描述的 find_spec() 协议,但它公开了额外的钩子,可用于自定义如何从导入路径查找和加载模块。

基于路径的查找器使用了三个变量:sys.pathsys.path_hookssys.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 中的每个可调用对象。此列表中的每个路径条目钩子都使用单个参数(要搜索的路径条目)调用。此可调用对象可能会返回一个可以处理路径条目的路径条目查找器,或者可能引发 ImportErrorImportError 由基于路径的查找器使用,以表示钩子无法为该路径条目找到路径条目查找器。该异常会被忽略,并且导入路径迭代会继续。该钩子应预期为字符串或字节对象;字节对象的编码由钩子决定(例如,它可能是文件系统编码、UTF-8 或其他编码),如果钩子无法解码该参数,则应引发 ImportError

如果 sys.path_hooks 迭代结束时没有返回路径条目查找器,则基于路径的查找器的 find_spec() 方法将 None 存储在 sys.path_importer_cache 中(以指示此路径条目没有查找器),并返回 None,表明此元路径查找器无法找到该模块。

如果 sys.path_hooks 上的可调用对象之一返回了路径条目查找器,则使用以下协议向查找器请求模块规范,该规范在加载模块时使用。

当前工作目录(用空字符串表示)的处理方式与其他 sys.path 中的条目略有不同。首先,如果发现当前工作目录不存在,则不会在 sys.path_importer_cache 中存储任何值。其次,每次查找模块时都会重新查找当前工作目录的值。第三,用于 sys.path_importer_cache 的路径,以及由 importlib.machinery.PathFinder.find_spec() 返回的路径将是实际的当前工作目录,而不是空字符串。

5.5.2. 路径条目查找器协议

为了支持模块和已初始化包的导入,并为命名空间包贡献部分内容,路径条目查找器必须实现 find_spec() 方法。

find_spec() 接受两个参数:要导入的模块的完全限定名称,以及(可选的)目标模块。 find_spec() 返回模块的完整填充的规范。此规范将始终设置“loader”(有一个例外)。

为了向导入机制指示该规范表示一个命名空间 部分,路径条目查找器将 submodule_search_locations 设置为包含该部分的列表。

在 3.4 版本中变更: find_spec() 取代了 find_loader()find_module(),两者现在都已弃用,但如果未定义 find_spec(),则仍将使用它们。

较旧的路径条目查找器可能会实现这两个已弃用的方法之一,而不是 find_spec()。 为了向后兼容,这些方法仍然受到尊重。但是,如果在路径条目查找器上实现了 find_spec(),则会忽略传统方法。

find_loader() 接受一个参数,即要导入的模块的完全限定名称。 find_loader() 返回一个 2 元组,其中第一项是加载器,第二项是命名空间 部分

为了与其他导入协议的实现向后兼容,许多路径条目查找器还支持与元路径查找器相同的传统 find_module() 方法。但是,路径条目查找器 find_module() 方法永远不会使用 path 参数调用(它们应该从对路径钩子的初始调用中记录相应的路径信息)。

路径条目查找器上的 find_module() 方法已弃用,因为它不允许路径条目查找器为命名空间包贡献部分内容。 如果路径条目查找器上同时存在 find_loader()find_module(),则导入系统将始终优先调用 find_loader() 而不是 find_module()

在 3.10 版本中变更: 导入系统对 find_module()find_loader() 的调用将引发 ImportWarning

在 3.12 版本中变更: find_module()find_loader() 已被删除。

5.6. 替换标准导入系统

替换整个导入系统最可靠的机制是删除 sys.meta_path 的默认内容,并完全替换为自定义的元路径钩子。

如果只改变 import 语句的行为,而不影响其他访问导入系统的 API 是可以接受的,那么替换内置的 __import__() 函数可能就足够了。 此技术也可以在模块级别使用,以仅更改该模块中 import 语句的行为。

为了有选择地阻止从元路径上的早期钩子导入某些模块(而不是完全禁用标准导入系统),只需直接从 find_spec() 引发 ModuleNotFoundError,而不是返回 None。 后者表示元路径搜索应继续,而引发异常会立即终止搜索。

5.7. 包的相对导入

相对导入使用前导点。单个前导点表示相对导入,从当前包开始。 两个或多个前导点表示相对于当前包的父包的相对导入,第一个点之后的每个点表示一级。 例如,给定以下包布局

package/
    __init__.py
    subpackage1/
        __init__.py
        moduleX.py
        moduleY.py
    subpackage2/
        __init__.py
        moduleZ.py
    moduleA.py

subpackage1/moduleX.pysubpackage1/__init__.py 中,以下是有效的相对导入

from .moduleY import spam
from .moduleY import spam as ham
from . import moduleY
from ..subpackage1 import moduleY
from ..subpackage2.moduleZ import eggs
from ..moduleA import foo

绝对导入可以使用 import <>from <> import <> 语法,但相对导入只能使用第二种形式;原因是

import XXX.YYY.ZZZ

应该将 XXX.YYY.ZZZ 公开为可用的表达式,但 .moduleY 不是有效的表达式。

5.8. __main__ 的特殊注意事项

__main__ 模块是相对于 Python 导入系统的一种特殊情况。正如 其他地方 所述,__main__ 模块在解释器启动时直接初始化,很像 sysbuiltins。但是,与这两个模块不同,它不严格符合内置模块的条件。这是因为 __main__ 的初始化方式取决于调用解释器时使用的标志和其他选项。

5.8.1. __main__.__spec__

根据 __main__ 的初始化方式,__main__.__spec__ 会被适当地设置或设置为 None

当 Python 使用 -m 选项启动时,__spec__ 将设置为相应模块或包的模块规范。 当 __main__ 模块作为执行目录、zip 文件或其他 sys.path 条目的一部分加载时,也会填充 __spec__

其余情况 中,__main__.__spec__ 设置为 None,因为用于填充 __main__ 的代码与可导入的模块不直接对应。

  • 交互式提示

  • -c 选项

  • 从标准输入运行

  • 直接从源代码或字节码文件运行

请注意,在最后一种情况下,__main__.__spec__ 始终为 None即使该文件在技术上可以被直接导入为一个模块。如果希望在 __main__ 中获得有效的模块元数据,请使用 -m 开关。

另请注意,即使 __main__ 对应于一个可导入的模块,并且 __main__.__spec__ 也被相应设置,它们仍然被认为是不同的模块。 这是因为由 if __name__ == "__main__": 保护的代码块仅在模块被用来填充 __main__ 命名空间时执行,而不是在正常导入期间执行。

5.9. 参考

自 Python 早期以来,导入机制已经有了显著的发展。 最初的 包规范 仍然可以阅读,尽管自该文档编写以来,一些细节已经发生了变化。

sys.meta_path 的最初规范是 PEP 302,随后在 PEP 420 中进行了扩展。

PEP 420 为 Python 3.3 引入了 命名空间包PEP 420 还引入了 find_loader() 协议,作为 find_module() 的替代方案。

PEP 366 描述了为主模块显式相对导入添加 __package__ 属性。

PEP 328 引入了绝对和显式相对导入,并最初提议使用 __name__ 来实现语义 PEP 366 最终将为 __package__ 指定。

PEP 338 定义了将模块作为脚本执行。

PEP 451 在规范对象中增加了每个模块导入状态的封装。它还将加载器的大部分样板职责转移回了导入机制。 这些更改允许弃用导入系统中的几个 API,并向查找器和加载器添加新方法。

脚注