5. 导入系统 — Python 文档

来自菜鸟教程
Python/docs/3.6/reference/import
跳转至:导航、​搜索

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 的标准属性访问语法。 因此,您可能有一个名为 sys 的模块和一个名为 email 的包,后者又具有一个名为 email.mime 的子包和一个名为 email.mime 的子包中的模块X187X]。

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.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 的导入协议来查找和加载模块。 该协议由两个概念对象组成,findersloaders。 finder 的工作是确定它是否可以使用它所知道的任何策略找到命名模块。 实现这两个接口的对象被称为 importers——它们在发现可以加载请求的模块时返回自身。

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

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

Finders 实际上并不加载模块。 如果他们能找到命名的模块,他们会返回一个 模块规范 ,这是模块导入相关信息的封装,然后导入机制在加载模块时使用。

以下部分更详细地描述了 finder 和 loader 的协议,包括如何创建和注册新的协议以扩展导入机制。

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


5.3.3. 导入挂钩

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

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

导入路径挂钩作为 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() 时,导入机器才会尝试它。


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 中存在具有给定名称的现有模块对象,则导入将已经返回它。
  • 在加载程序执行模块代码之前,模块将存在于 sys.modules 中。 这很关键,因为模块代码可以(直接或间接)导入自身; 预先将其添加到 sys.modules 可防止最坏情况下的无限递归和最佳情况下的多次加载。
  • 如果加载失败,则会从 sys.modules 中删除失败的模块——并且只有失败的模块。 任何已经在 sys.modules 缓存中的模块,以及作为副作用成功加载的任何模块,都必须保留在缓存中。 这与在 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() 期间引发的任何其他异常都将被传播。


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

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

3.4 新功能: 加载器的 create_module() 方法。


3.4 版更改: load_module() 方法被替换为 exec_module() 并且导入机制承担了所有样板加载责任。

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

load_module() 方法除了执行模块外,还必须实现上述所有样板加载功能。 所有相同的限制都适用,但有一些额外的说明:

  • 如果在 sys.modules 中存在具有给定名称的现有模块对象,则加载程序必须使用该现有模块。 (否则,importlib.reload() 将无法正常工作。)如果命名模块在 sys.modules 中不存在,则加载器必须创建一个新的模块对象并将其添加到sys.modules
  • 在加载器执行模块代码之前,模块必须存在于sys.modules中,以防止无限递归或多次加载。
  • 如果加载失败,加载器必须删除它插入到 sys.modules 中的任何模块,但它必须删除 only 失败的模块,并且只有当加载器本身已经加载明确的模块。


在 3.5 版中更改:A DeprecationWarning 在定义 exec_module() 但未定义 create_module() 时引发。


在 3.6 版中更改:ImportError 在定义 exec_module() 但未定义 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__ 属性。 有关模块规范内容的详细信息,请参阅 ModuleSpec

3.4 版中的新功能。


5.4.5. 模块.__路径__

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

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

__path__ 必须是一个可迭代的字符串,但可以为空。 用于 sys.path 的相同规则也适用于包的 __path__sys.path_hooks(如下所述)在遍历包的 [ X171X]。

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


5.4.6. 模块代表

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

如果模块有一个规范(__spec__),导入机制将尝试从中生成一个repr。 如果失败或没有规范,导入系统将使用模块上可用的任何信息制作默认代表。 它将尝试使用 module.__name__module.__file__module.__loader__ 作为 repr 的输入,对于缺少的任何信息均使用默认值。

以下是使用的确切规则:

  • 如果模块具有 __spec__ 属性,则使用规范中的信息生成 repr。 查询“name”、“loader”、“origin”和“has_location”属性。
  • 如果模块具有 __file__ 属性,则将其用作模块代表的一部分。
  • 如果模块没有 __file__ 但有一个不是 None__loader__,那么加载器的 repr 被用作模块的 repr 的一部分。
  • 否则,只需在 repr 中使用模块的 __name__


3.4 版更改: loader.module_repr() 的使用已被弃用,导入机制现在使用模块规范来生成模块代表。

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


5.5. 基于路径的查找器

如前所述,Python 带有几个默认的元路径查找器。 其中一个称为基于 路径的查找器 (PathFinder),搜索 导入路径 ,其中包含 路径条目 的列表。 每个路径条目命名一个位置来搜索模块。

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

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

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

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

警告:本节和上一节都使用术语finder,分别使用术语meta path finderpath entry 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()[来开始导入路径搜索X176X] 方法,如前所述。 当给出 find_spec()path 参数时,它将是要遍历的字符串路径列表 - 通常是包中导入的包的 __path__ 属性。 如果 path 参数是 None,这表示顶级导入并使用 sys.path

基于路径的查找器遍历搜索路径中的每个条目,并为每个条目查找路径条目的适当 路径条目查找器 (PathEntryFinder)。 因为这可能是一项昂贵的操作(例如 此搜索可能有 stat() 调用开销),基于路径的查找器维护一个缓存映射路径条目到路径条目查找器。 该缓存在 sys.path_importer_cache 中维护(尽管名称如此,但该缓存实际上存储了 finder 对象,而不是仅限于 importer 对象)。 这样,对特定 路径条目 位置的 路径条目查找器 的昂贵搜索只需执行一次。 用户代码可以自由地从 sys.path_importer_cache 中删除缓存条目,迫使基于路径的查找器再次执行路径条目搜索 3

如果缓存中不存在路径条目,则基于路径的查找器将遍历 sys.path_hooks 中的每个可调用项。 此列表中的每个 路径条目 hooks 都使用单个参数调用,即要搜索的路径条目。 这个可调用对象可能会返回一个 路径条目查找器 来处理路径条目,或者它可能会引发 ImportError。 基于路径的查找器使用 ImportError 来表示钩子无法找到该 路径条目路径条目查找器 。 异常被忽略,import path 迭代继续。 钩子应该接受字符串或字节对象; 字节对象的编码取决于钩子(例如 它可能是文件系统编码、UTF-8 或其他),如果钩子无法解码参数,它应该引发 ImportError

如果 sys.path_hooks 迭代结束时没有返回 path entry finder,则基于路径的 finder 的 find_spec() 方法将存储 None sys.path_importer_cache(表示该路径项没有finder)并返回None,表示这个meta path finder找不到模块。

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

当前工作目录(由空字符串表示)的处理方式与 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”设置为 None 并将“submodule_search_locations”设置为包含该部分的列表。

3.4 版更改:find_spec() 替换了 find_loader()find_module(),这两个现在都已弃用,但会如果 find_spec() 未定义,则使用。

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

find_loader() 接受一个参数,即被导入模块的完全限定名称。 find_loader() 返回一个二元组,其中第一项是加载器,第二项是命名空间 部分 。 当第一项(即 加载器)是 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 的默认内容,用自定义元路径挂钩完全替换它们。

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

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


5.7. __main__ 的特殊注意事项

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

5.7.1. __main__.__spec__

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

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

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

  • 交互式提示
  • -c 选项
  • 从标准输入运行
  • 直接从源文件或字节码文件运行

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

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


5.8. 开放式问题

XXX 要是有图就好了。

XXX * (import_machinery.rst) 专门讨论模块和包的属性的部分怎么样,也许扩展或取代数据模型参考页面中的相关条目?

库手册中的 XXX runpy、pkgutil 等都应该在顶部获得指向新导入系统部分的“另请参阅”链接。

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 引入了绝对和显式相对导入,最初提出 __name__ 用于语义 PEP 366 最终将指定 [ X178X]。

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

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

脚注

1
参见 types.ModuleType
2
importlib 实现避免直接使用返回值。 相反,它通过在 sys.modules 中查找模块名称来获取模块对象。 这样做的间接影响是导入的模块可能会在 sys.modules 中替换自身。 这是特定于实现的行为,不能保证在其他 Python 实现中工作。
3
在遗留代码中,可以在 sys.path_importer_cache 中找到 imp.NullImporter 的实例。 建议将代码更改为使用 None。 有关更多详细信息,请参阅 移植 Python 代码