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 都会为其创建一个命名空间包。

另请参阅 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.bazsys.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 标识的资源。

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

查找器实际上不加载模块。如果它们能找到指定的模块,它们会返回一个模块规格(module spec),这是模块导入相关信息的封装,导入机制在加载模块时会使用它。

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

在 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 中已存在具有给定名称的模块对象,import 将已经返回了它。

  • 在加载器执行模块代码之前,该模块就会存在于 sys.modules 中。这一点至关重要,因为模块代码可能会(直接或间接)导入自身;事先将其添加到 sys.modules 中可以防止最坏情况下的无限递归和最好情况下的多次加载。

  • 如果加载失败,失败的模块——且仅有失败的模块——会从 sys.modules 中移除。任何已在 sys.modules 缓存中的模块,以及任何作为副作用成功加载的模块,都必须保留在缓存中。这与重载(reloading)不同,在重载中,即使是失败的模块也会留在 sys.modules 中。

  • 在模块创建之后、执行之前,导入机制会设置与导入相关的模块属性(在上面的伪代码示例中为“_init_module_attrs”),具体内容在后面的章节中总结。

  • 模块执行是加载的关键时刻,模块的命名空间在此期间被填充。执行完全委托给加载器,由加载器决定填充什么内容以及如何填充。

  • 在加载期间创建并传递给 exec_module() 的模块,可能不是在 import 结束时返回的那个模块[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() 已被弃用,加载器应改为实现 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__ 但有一个不为 None__loader__,那么加载器的 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 简单地假设如果缓存文件存在,它就是有效的。基于哈希的 .pyc 文件验证行为可以通过 --check-hash-based-pycs 标志来覆盖。

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

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.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 中的每个可调用对象。此列表中的每个路径条目钩子都会被调用,并传入一个参数,即要搜索的路径条目。这个可调用对象可以返回一个能够处理该路径条目的路径条目查找器,也可以引发 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() 已被移除。

5.6. 替换标准的导入系统

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

如果只改变导入语句的行为而不影响访问导入系统的其他 API 是可以接受的,那么替换内置的 __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__ 的特殊注意事项

相对于 Python 的导入系统,__main__ 模块是一个特例。正如在别处提到的,__main__ 模块在解释器启动时直接初始化,很像 sysbuiltins。然而,与那两者不同,它并不严格地算作一个内置模块。这是因为 __main__ 的初始化方式取决于调用解释器时使用的标志和其他选项。

5.8.1. __main__.__spec__

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

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

其余情况下__main__.__spec__ 被设置为 None,因为用于填充 __main__ 的代码并不直接对应一个可导入的模块:

  • 交互式提示符

  • -c 选项

  • 从 stdin 运行

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

请注意,在最后一种情况下,__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 添加了将每个模块的导入状态封装在 spec 对象中。它还将加载器的大部分样板职责交还给了导入机制。这些更改使得导入系统中的几个 API 被弃用,并为查找器和加载器添加了新方法。

脚注