Skip to main content

5. 进口系统

在一个 module 中的Python代码通过 importing 的过程获得对另一个模块中的代码的访问。 import 语句是调用导入机制的最常用的方法,但它不是唯一的方法。诸如 importlib.import_module() 和内置 __import__() 的功能也可以用于调用导入机制。

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

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

当作为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的标准属性访问语法。因此,您可能有一个名为 sys 的模块和一个名为 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 旁边。在这种情况下,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 中的缓存条目无效,然后重新导入命名的模块,两个模块对象将 not 相同。相比之下,importlib.reload() 将重用 same 模块对象,并通过重新运行模块的代码来重新初始化模块内容。

5.3.2. 查找器和装载器

如果在 sys.modules 中找不到指定的模块,那么将调用Python的导入协议来查找和加载模块。该协议包括两个概念对象,finders装载机。 finder的工作是确定它是否可以使用知道的任何策略找到命名的模块。实现这两个接口的对象称为 进口商 - 当他们发现他们可以加载请求的模块时,它们返回自己。

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

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

查找器实际上不加载模块。如果他们可以找到命名的模块,他们返回一个 module spec,模块的导入相关信息的封装,导入机制然后在加载模块时使用。

以下部分更详细地介绍查找器和装载程序的协议,包括如何创建和注册新协议以扩展导入机制。

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

5.3.3. 导入挂钩

进口机械设计为可扩展;其主要机制是 导入钩子。有两种类型的导入钩子:元钩导入路径钩子

在导入处理开始时,在发生任何其他导入处理之前调用元钩子,而不是 sys.modules 高速缓存查找。这允许元钩子重写 sys.path 处理,冻结模块,甚至内置模块。通过向 sys.meta_path 添加新的finder对象来注册元钩子,如下所述。

导入路径挂钩称为 sys.path (或 package.__path__)处理的一部分,在那里遇到它们相关的路径项目点。导入路径挂钩由下述增加新的可调用到 sys.path_hooks 注册。

5.3.4. 元路径

当在 sys.modules 中找不到命名的模块时,Python接下来搜索 sys.meta_path,其中包含元路径查找器对象的列表。这些查找器被查询以查看他们是否知道如何处理命名的模块。元路径查找器必须实现一个称为 find_spec() 的方法,它接受三个参数:名称,导入路径和(可选)目标模块。元路径查找器可以使用它想要的任何策略来确定它是否可以处理命名的模块。

如果元路径查找器知道如何处理指定的模块,它返回一个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()

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:
    if spec.submodule_search_locations is not None:
        # namespace package
        sys.modules[spec.name] = module
    else:
        # unsupported
        raise ImportError
elif not hasattr(spec.loader, 'exec_module'):
    module = spec.loader.load_module(spec.name)
    # Set __loader__ and __package__ if missing.
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 缓存中的任何模块,以及作为副作用成功加载的任何模块必须保留在缓存中。这与重新加载形成对比,其中甚至故障模块留在 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() 期间引发的任何其他异常将被传播。

在许多情况下,finder和loader可以是同一个对象;在这种情况下,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

  • 在加载器执行模块代码之前,模块 must 存在于 sys.modules 中,以防止无界递归或多重加载。

  • 如果加载失败,加载器必须删除它插入到 sys.modules 中的任何模块,但它必须删除失败的模块的 只要,并且只有加载器本身已经明确加载了模块。

在 3.5 版更改: 当定义 exec_module() 但不定义 create_module() 时,产生 DeprecationWarning。从Python 3.6开始,在与ModuleSpec连接的加载器上不定义 create_module() 将是一个错误。

5.4.2. 子模块

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

spam/
    __init__.py
    foo.py
    bar.py

spam/__init__.py 有以下几行:

from .foo import Foo
from .bar import Bar

然后执行以下命令,将名称绑定到 spam 模块中的 foobar:

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

给定Python熟悉的名称绑定规则,这可能看起来令人惊讶,但它实际上是导入系统的一个基本特征。不变的控制是,如果你有 sys.modules['spam']sys.modules['spam.foo'] (如上述导入之后),后者必须显示为前者的 foo 属性。

5.4.3. 模块规格

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

在导入期间使用规范允许状态在导入系统组件之间传送,例如,在创建模块spec的finder和执行它的loader之间。最重要的是,它允许导入机器执行加载的样板操作,而没有模块规范,加载器有责任。

有关模块规格可能包含的信息的更多细节,请参阅 ModuleSpec

3.4 新版功能.

5.4.5. 模块.__路径___

根据定义,如果模块具有 __path__ 属性,则它是一个包,而不管其值。

包的 __path__ 属性在其子包的导入期间使用。在进口机制内,其功能与 sys.path 大致相同,即提供在导入期间搜索模块的位置列表。然而,__path__ 通常比 sys.path 更受限制。

__path__ 必须是可迭代的字符串,但它可能为空。用于 sys.path 的相同规则也适用于封装的 __path__,并且当遍历封装的 __path__ 时参考 sys.path_hooks (如下所述)。

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

5.4.6. 模块reprs

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

如果模块有一个规范(__spec__),导入机制将尝试从它生成一个repr。如果失败或没有spec,导入系统将使用模块上可用的任何信息来创建默认repr。它将尝试使用 module.__name__module.__file__module.__loader__ 作为repr的输入,默认情况下缺少任何信息。

这里是使用的确切规则:

  • 如果模块具有 __spec__ 属性,规范中的信息用于生成repr。参考“名称”,“加载器”,“原点”和“has_location”属性。

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

  • 如果模块没有 __file__,但有一个不是 None__loader__,那么加载器的repr被用作模块的repr的一部分。

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

在 3.4 版更改: 使用 loader.module_repr() 已被弃用,模块规格现在被导入机制用于生成模块repr。

为了与Python 3.3向后兼容,在尝试上述任一方法之前,将通过调用加载器的 module_repr() 方法(如果已定义)来生成模块repr。但是,该方法已弃用。

5.5. 基于路径的查找器

如前所述,Python附带了几个默认元路径查找器。其中之一,称为 path based finderPathFinder),搜索 import path,其中包含 路径条目 的列表。每个路径条目命名一个位置以搜索模块。

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

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

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

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

警告词:本节和前面两个使用术语 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 findersys.pathsys.path_hookssys.path_importer_cache 使用三个变量。也使用包对象上的 __path__ 属性。这些提供了进口机制可以定制的额外方式。

sys.path 包含提供模块和包的搜索位置的字符串列表。它从 PYTHONPATH 环境变量和各种其他安装和实现特定的默认值初始化。 sys.path 中的条目可以命名文件系统上的目录,zip文件以及可能需要搜索模块(如URL或数据库查询)的其他“位置”(参见 site 模块)。只有字符串和字节应存在于 sys.path 上;所有其他数据类型将被忽略。字节条目的编码由各个 路径条目查找器 确定。

path based findermeta path finder,因此导入机制通过调用基于路径的finder的 find_spec() 方法开始 import path 搜索,如前所述。当给出 find_spec()path 参数时,它将是要遍历的字符串路径的列表 - 通常是该包内的导入的包的 __path__ 属性。如果 path 参数为 None,则表示使用顶级导入和 sys.path

基于路径的查找器遍历搜索路径中的每个条目,并且对于每个条目,为路径条目寻找合适的 path entry finderPathEntryFinder)。因为这可能是昂贵的操作(例如,对于该搜索可能存在 stat() 调用开销),所以基于路径的查找器维护到路径条目查找器的高速缓存映射路径条目。这个高速缓存在 sys.path_importer_cache 中维护(尽管名字,这个高速缓存实际上存储finder对象,而不是被限制为 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() 方法将 None 存储在 sys.path_importer_cache 中(以指示对于该路径条目没有查找器),并返回 None,指示该 meta path finder 找不到模块。

如果 path entry finder issys.path_hooks 上的 path entry hook 可调用程序之一返回,则使用以下协议向查询器询问模块规格,然后在加载模块时使用该规范。

当前工作目录(由空字符串表示)与 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”设置(有一个例外)。

向进口机制指示规范表示命名空间 portion。路径入口查找器将规范上的“loader”设置为 None,将“submodule_search_locations”设置为包含该部分的列表。

在 3.4 版更改: find_spec() 替换了 find_loader()find_module(),两者现在已被废弃,但如果未定义 find_spec(),将使用它们。

较旧的路径条目查找器可以实现这两种不建议使用的方法之一,而不是 find_spec()。该方法仍然是为了向后兼容性的考虑。然而,如果在路径入口查找器上实现 find_spec(),则遗弃方法被忽略。

find_loader() 接受一个参数,即正在导入的模块的完全限定名称。 find_loader() 返回一个2元组,其中第一个项目是加载器,第二个项目是一个命名空间 portion。当第一项(即加载程序)是 None 时,这意味着当路径入口查找器没有用于命名模块的加载器时,它知道该路径入口有助于命名模块的命名空间部分。这将几乎总是这样的情况,其中要求Python导入在文件系统上没有物理存在的命名空间包。当路径条目查找器返回加载器的 None 时,2元组返回值的第二项必须是序列,尽管它可以为空。

如果 find_loader() 返回非 None 加载程序值,则忽略该部分,并且从基于路径的查找器返回加载程序,从而终止通过路径条目的搜索。

为了向后兼容导入协议的其他实现,许多路径条目查找器还支持元路径查找器支持的相同的传统 find_module() 方法。然而,路径条目查找器 find_module() 方法从不用 path 参数调用(它们被期望记录从初始调用到路径钩子的适当路径信息)。

路径条目查找器上的 find_module() 方法已弃用,因为它不允许路径条目查找器对命名空间包提供部分。如果路径入口查找器上存在 find_loader()find_module(),则导入系统将始终优先于 find_module() 调用 find_loader()

5.6. 更换标准导入系统

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

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

为了选择性地阻止在元路径上早期从钩子中导入一些模块(而不是完全禁用标准导入系统),从 find_spec() 直接提交 ModuleNoFoundError 而不是返回 None 就足够了。后者指示元路径搜索应该继续,而提高异常立即终止它。

5.7. __main__的特殊注意事项

__main__ 模块是相对于Python的导入系统的特殊情况。如 别处 所述,__main__ 模块在解释器启动时直接初始化,非常类似于 sysbuiltins。然而,与这两个不同,它不严格限定为内置模块。这是因为 __main__ 初始化的方式取决于调用解释器的标志和其他选项。

5.7.1. __main __.__ spec__

根据 __main__ 如何初始化,__main__.__spec__ 被适当地设置或 None

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

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

  • 交互式提示

  • -c开关

  • 从stdin运行

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

注意,在最后一种情况下 __main__.__spec__ 总是 None即使 文件可以在技术上直接作为模块导入。如果在 __main__ 中需要有效的模块元数据,请使用 -m 开关。

还要注意,即使当 __main__ 对应于可导入模块并且相应地设置 __main__.__spec__ 时,它们仍然被认为是 distinct 模块。这是因为由 if __name__ == "__main__": 检查保护的块仅在模块用于填充 __main__ 命名空间时执行,而不是在正常导入期间执行。

5.8. 开放式问题

XXX这将是非常好的有一个图。

XXX * (import_machinery.rst)一个专门介绍模块和包的属性的部分,或者扩展或替换数据模型参考页中的相关条目?

XXX runpy,pkgutil等在库手册中应该都得到“See Also”链接在顶部指向新的导入系统部分。

XXX添加关于 __main__ 初始化的不同方式的更多解释?

XXX添加更多信息关于 __main__ 怪异/陷阱(即从 PEP 395 复制)。

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 引入绝对和显式相对导入,并且最初提出的用于语义 PEP 366__name__ 将最终指定 __package__

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

PEP 451 在spec对象中添加了每个模块导入状态的封装。它还将装载机的大部分样板责任卸载回进口机械。这些更改允许在导入系统中弃用多个API,并向查找器和装载器添加新方法。

脚注

[1]

types.ModuleType

[2]

importlib实现避免直接使用返回值。相反,它通过在 sys.modules 中查找模块名称来获取模块对象。其间接效果是,导入的模块可以在 sys.modules 中替换自身。这是实现特定的行为,不能保证在其他Python实现中工作。

[3]

在遗留代码中,可以在 sys.path_importer_cache 中找到 imp.NullImporter 的实例。建议将代码更改为使用 None。详情请参阅 移植Python代码