5.导入系统

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

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

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

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

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

版本3.3中已更改:导入系统已更新为完全实现第二阶段PEP 302。不再有任何隐含的导入机制 – 完整的导入系统会暴露出来sys.meta_path。此外,还实现了本机命名空间包支持(请参阅PEP 420)。

5.1。importlib

importlib模块提供了丰富的API,用于与导入系统进行交互。例如,importlib.import_module()提供了一个推荐的,比内置更简单的API __import__()来调用导入机制。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__.py和 parent/one/__init__.py。的后续进口parent.twoparent.three将执行parent/two/__init__.py,并 parent/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则引发a。

5.3.1。模块缓存

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

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

sys.modules是可写的。删除密钥可能不会破坏关联的模块(因为其他模块可能会保留对它的引用),但它会使命名模块的缓存条目无效,导致Python在下次导入时重新搜索命名模块。密钥也可以分配给None,强制下一次导入模块导致a ModuleNotFoundError

但要注意,就好像你保留对模块对象的引用,使其缓存条目无效sys.modules,然后重新导入命名模块,两个模块对象将一样。相反, importlib.reload()将重用相同的模块对象,并通过重新运行模块的代码简单地重新初始化模块内容。

5.3.2。查找程序和加载程序

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

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

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

Finder实际上并没有加载模块。如果他们可以找到命名模块,则返回模块规范,模块的导入相关信息的封装,然后导入机器在加载模块时使用。

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

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

5.3.3。导入钩子

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

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

导入路径挂钩在遇到其关联路径项的位置被称为sys.path(或 package.__path__)处理的一部分。通过添加新的callables来注册导入路径挂钩,sys.path_hooks如下所述。

5.3.4。元路径

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

如果元路径查找器知道如何处理命名模块,它将返回一个spec对象。如果它无法处理命名模块,则返回None。如果 sys.meta_path处理到达其列表的末尾而没有返回规范,则ModuleNotFoundError引发a。引发的任何其他异常都会简单地传播,中止导入过程。

find_spec()使用两个或三个参数调用元路径查找器的方法。例如,第一个是要导入的模块的完全限定名称foo.bar.baz。第二个参数是用于模块搜索的路径条目。对于顶级模块,第二个参数是None,但对于子模块或子包,第二个参数是父包的__path__属性的值 。如果__path__无法访问适当的属性,ModuleNotFoundError则引发a。第三个参数是一个现有的模块对象,它将成为稍后加载的目标。导入系统仅在重新加载期间传入目标模块。

对于单个导入请求,可以多次遍历元路径。例如,假设所涉及的模块都没有被缓存,导入foo.bar.baz将首先执行顶级导入,调用 每个元路径查找器()。后 已导入,将通过遍历元路径的第二时间,呼叫进口 。一旦导入,最终遍历将调用 。mpf.find_spec("foo", None, None)mpffoofoo.barmpf.find_spec("foo.bar", foo.__path__,None)foo.barmpf.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.modulesimport将已返回它。
  • 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()已经弃用并且加载器应该实现exec_module()

load_module()除了执行模块之外,该方法还必须实现上述所有样板加载功能。所有相同的限制条件都适用,并进一步澄清:

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

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

在版本3.6中更改:定义ImportError时引发An exec_module()但 create_module()不是。

5.4.2。子模块

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

spam/ 
    __init__.py 
    foo.py 
    bar.py

 

spam/__init__.py在其中包含以下内容:

from .foo import Foo 
from .bar import Bar

 

然后执行以下把将名称绑定到foobar所述在 spam模块:

>>>
>>> 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__属性,则它是一个包。

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

__path__必须是可迭代的字符串,但它可能是空的。使用的相同规则sys.path也适用于包 __path__,并且sys.path_hooks在遍历包时咨询(如下所述)__path__

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

5.4.6。模块代表

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

如果模块有spec(__spec__),导入机器将尝试从中生成repr。如果失败或没有规范,导入系统将使用模块上可用的任何信息制作默认repr。它会尝试使用module.__name__, module.__file__以及module.__loader__作为输入到再版,用默认设置的任何信息丢失。

以下是使用的确切规则:

  • 如果模块具有__spec__属性,则使用规范中的信息生成repr。查阅“名称”,“加载器”,“原点”和“has_location”属性。
  • 如果模块具有__file__属性,则将其用作模块的repr的一部分。
  • 如果模块没有,__file__但确实__loader__没有 None,则加载器的repr用作模块repr的一部分。
  • 否则,只需使用__name__repr中的模块。

在3.4版本中更改:使用loader.module_repr() 已被弃用,模块规范现在所使用的进口机器,生成模块再版。

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

5.4.7。缓存的字节码失效

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

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

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

5.5。基于路径的查找器

如前所述,Python附带了几个默认的元路径查找器。其中一个称为基于路径的finder (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.path, sys.path_hookssys.path_importer_cache__path__ 还使用包对象上的属性。这些提供了可以定制进口机械的其他方式。

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

基于路径的查找程序元路径查找程序,因此导入机制通过调用基于路径的查找程序的方法开始导入路径搜索,find_spec()如前所述。当path以参数 find_spec()给出,这将是字符串路径列表遍历-通常是包的__path__ 属性对于包内的进口。如果path参数为 None,则表示使用顶级导入sys.path

基于路径的查找器迭代搜索路径中的每个条目,并且对于每个条目,查找路径条目的适当路径条目查找器PathEntryFinder)。因为这可能是一项昂贵的操作(例如,此搜索可能存在 stat()调用开销),所以基于路径的查找器维护到路径条目查找器的缓存映射路径条目。维护此缓存sys.path_importer_cache(尽管名称,此缓存实际上存储查找器对象而不是限于导入器对象)。以这种方式,对特定路径入口 位置的路径入口查找器的昂贵搜索仅需要进行一次。用户代码可以从中删除缓存条目sys.path_importer_cache强制基于路径的查找器再次执行路径条目搜索[3]

如果高速缓存中不存在路径条目,则基于路径的查找器将遍历每个可调用对象sys.path_hooks。使用单个参数(要搜索的路径条目)调用此列表中的每个路径条目挂钩。此可调用可以返回可以处理路径条目的路径条目查找器,也可以引发它 ImportError。一个ImportError是使用的基于路径查找器发出信号,表明挂钩找不到路径条目取景器路径条目。忽略该异常并导入路径迭代继续。钩子应该是一个字符串或字节对象; 字节对象的编码取决于钩子(例如,它可能是文件系统编码,UTF-8或其他东西),如果钩子不能解码参数,它应该引发 ImportError

如果sys.path_hooks迭代没有结束路径条目取景器 返回,则路径基于Finder的 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()返回模块的完全填充规范。此规范将始终设置“加载器”(有一个例外)。

向导入机器指示规范表示命名空间 部分。路径条目查找器将spec上的“loader” None和“submodule_search_locations”设置为包含该部分的列表。

改变在3.4版本:find_spec()更换 find_loader()和 find_module(),这两者现在都过时了,但如果将使用find_spec()没有定义。

较旧的路径条目查找器可以实现这两种弃用方法中的一种而不是find_spec()。为了向后兼容,仍然遵守这些方法。但是,如果find_spec()在路径条目查找器上实现,则忽略旧方法。

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

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

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

find_module()不推荐使用路径条目查找器上的方法,因为它不允许路径条目查找器向命名空间包提供部分。如果两个find_loader()find_module() 一个路径条目取景器存在,导入系统总是会调用 find_loader()优先于find_module()

5.6。替换标准导入系统

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

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

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

5.7。__main__的特殊注意事项

__main__模块是相对于Python导入系统的特殊情况。正如指出的其他地方,该__main__模块直接初始化在翻译的启动,就像sys和 builtins。但是,与这两者不同,它并不严格限定为内置模块。这是因为__main__初始化的方式 取决于调用解释器的标志和其他选项。

5.7.1。__main __.__ spec__ 

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

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

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

  • 互动提示
  • -c 选项
  • 从stdin运行
  • 直接从源或字节码文件运行

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

另请注意,即使__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_pathPEP 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,并为查找程序和加载程序添加新方法。

脚注

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