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.find_spec("foo", None, None)mpf)。在导入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. 模块规范

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

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

模块的规范作为模块对象上的 __spec__ 属性公开。有关模块规范内容的详细信息,请参见 ModuleSpec

在版本 3.4 中添加。

5.4.5. module.__path__

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

包的 __path__ 属性在导入其子包时使用。在导入机制中,它的作用与 sys.path 类似,即提供一个在导入期间搜索模块的位置列表。但是,__path__ 通常比 sys.path 更加受限。

__path__ 必须是字符串的可迭代对象,但可以为空。用于 sys.path 的相同规则也适用于包的 __path__,并且 sys.path_hooks(下面描述)在遍历包的 __path__ 时会被查询。

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

5.4.6. 模块 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.7. 缓存字节码失效

在 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 语义以在网络上查找模块的钩子。此钩子(一个可调用对象)将返回一个 路径条目查找器,它支持下面描述的协议,然后使用它从网络获取模块的加载器。

警告:本节和上一节都使用了“查找器”一词,并使用“元路径查找器”和“路径条目查找器”来区分它们。这两种类型的查找器非常相似,支持类似的协议,并且在导入过程中以类似的方式工作,但重要的是要记住它们之间存在细微的差别。特别是,元路径查找器在导入过程的开始阶段运行,以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 上返回了路径条目查找器,则将使用以下协议来向查找器请求模块规范,然后在加载模块时使用该规范。

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

5.5.2. 路径条目查找器协议

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

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

为了向导入机制指示规范表示命名空间 部分,路径条目查找器将 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 的默认内容,并用自定义元路径钩子完全替换它们。

如果可以接受仅更改导入语句的行为,而不影响访问导入系统的其他 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__ 的特殊注意事项

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

5.8.1. __main__.__spec__

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

当 Python 使用 -m 选项启动时,__spec__ 会被设置为相应模块或包的模块规范。当 __main__ 模块作为执行目录、压缩文件或其他 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,并添加了查找器和加载程序的新方法。

脚注