Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 451 – 导入系统中的 ModuleSpec 类型

作者:
Eric Snow <ericsnowcurrently at gmail.com>
BDFL 代表:
Brett Cannon <brett at python.org>, Alyssa Coghlan <ncoghlan at gmail.com>
讨论邮件列表:
Import-SIG 列表
状态:
最终
类型:
标准跟踪
创建:
2013 年 8 月 8 日
Python 版本:
3.4
历史记录:
2013 年 8 月 8 日,2013 年 8 月 28 日,2013 年 9 月 18 日,2013 年 9 月 24 日,2013 年 10 月 4 日
决议:
Python-Dev 邮件

目录

摘要

本 PEP 提出向 importlib.machinery 添加一个名为“ModuleSpec”的新类。它将提供加载模块所需的所有与导入相关的信息,并且无需先加载模块即可使用。查找器将直接提供模块的规范,而不是加载器(它们将继续间接提供加载器)。导入机制将进行调整以利用模块规范,包括使用它们来加载模块。

术语和概念

此提案中的更改为使几个现有的术语和概念更加清晰提供了机会,而目前这些术语和概念(不幸的是)是模棱两可的。此提案中还引入了新概念。最后,值得解释一些其他现有的术语,人们可能不太熟悉。为了提供上下文,以下是所有三组术语和概念的简要概述。在[2]中可以找到对导入系统的更详细解释。

名称

在此提案中,模块的“名称”指的是其完全限定名称,这意味着模块的父级(如果有)的完全限定名称与模块的简单名称通过句点连接。

查找器

“查找器”是一个对象,它识别导入系统应使用哪个加载器来加载模块。目前,这是通过调用查找器的 find_module() 方法来完成的,该方法返回加载器。

查找器严格负责提供加载器,它们通过其 find_module() 方法来做到这一点。然后,导入系统使用该加载器来加载模块。

加载器

“加载器”是在导入期间用于加载模块的对象。目前,这是通过调用加载器的 load_module() 方法来完成的。加载器还可以提供 API 用于获取有关其可以加载的模块的信息,以及有关与该模块关联的源的数据。

现在,加载器(通过 load_module())负责某些与导入相关的样板操作。这些是

  1. 执行一些(与模块相关的)验证
  2. 创建模块对象
  3. 在模块上设置与导入相关的属性
  4. 将模块“注册”到 sys.modules
  5. 执行模块
  6. 在加载模块期间发生故障时进行清理

所有这些都发生在导入系统调用 Loader.load_module() 期间。

来源

这是一个新的术语和概念。它的概念在导入系统中已经微妙地存在,但是此提案使该概念明确。

在导入上下文中,“来源”指的是模块源自的系统(或系统内的资源)。出于本提案的目的,“来源”也是一个字符串,用于标识此类资源或系统。“来源”适用于所有模块。

例如,内置模块和冻结模块的来源是解释器本身。导入系统已经将此来源分别标识为“内置”和“冻结”。这在以下模块表示中得到证明:“<module ‘sys’ (built-in)>” 。

实际上,模块表示已经是模块来源的相对可靠的(尽管是隐式的)指标。其他模块也通过其他方式指示其来源,如“位置”条目中所述。

加载器决定如何解释和使用模块的来源(如果有)的责任。

位置

这是一个新术语。但是该概念已经在导入系统中清晰地存在,例如与模块的__file____path__属性以及其他地方的名称/术语“路径”相关联。

“位置”是加载模块的资源或“地方”,而不是整个系统。它有资格成为“来源”。位置的示例包括文件系统路径和 URL。位置由资源的名称标识,但不一定标识资源所属的系统。在这种情况下,加载器必须识别系统本身。

与其他类型的模块来源相比,加载器仅通过模块名称无法推断位置。相反,必须向加载器提供一个字符串来标识位置,通常由生成加载器的查找器提供。然后,加载器使用此信息来查找它将从中加载模块的资源。理论上,您可以在不同的名称下加载给定位置的模块。

导入系统中位置最常见的示例是加载源模块和扩展模块的文件。对于这些模块,位置由__file__属性中的字符串标识。虽然__file__对于某些模块(例如压缩文件)并不特别准确,但它目前是导入系统指示模块具有位置的唯一方法。

具有位置的模块可以称为“可定位的”。

缓存

导入系统将编译后的模块存储在 __pycache__ 目录中作为优化。我们今天使用的此模块缓存由PEP 3147提供。对于此提案,与模块缓存相关的相关 API 是模块的__cache__属性以及 importlib.util 中的 cache_from_source() 函数。加载器负责将模块放入缓存(以及从缓存中加载)。目前,缓存仅用于编译后的源模块。但是,加载器可以利用模块缓存用于其他类型的模块。

该概念没有改变,术语也没有改变。但是,模块和包之间的区别大多是表面的。包模块。它们只是具有__path__属性,并且导入可能会添加绑定到子模块的属性。通常认为的差异是混淆的来源。此提案明确地弱化了模块和包之间的区别,在有意义的情况下这样做。

动机

导入系统在 Python 的生命周期中得到了发展。在 2002 年底,PEP 302通过查找器和加载器以及 sys.meta_path 引入了标准化的导入钩子。importlib 模块(从 Python 3.1 开始引入)现在公开了 PEP 302描述的 API 以及整个导入系统的纯 Python 实现。现在,理解和扩展导入系统变得容易得多。虽然这对 Python 社区来说是一个好处,但这种更大的可访问性也带来了挑战。

随着越来越多的开发人员开始理解和自定义导入系统,查找器和加载器 API 中的任何弱点都会产生更大的影响。因此,我们越早解决导入系统中的任何此类弱点越好……我们希望通过此提案解决其中的几个问题。

首先,任何时候导入系统需要保存有关模块的信息,我们最终都会在模块对象上添加更多属性,这些属性通常仅对导入系统有意义。最好有一个每个模块的命名空间,用于放置未来的与导入相关的信息并在导入系统内部传递。其次,在查找器和加载器之间存在 API 空白,这会导致遇到时出现不必要的复杂性。PEP 420(命名空间包)的实现不得不解决此问题。在最近针对单独提案的努力中,这种复杂性再次出现。[1]

上面的查找器加载器部分详细介绍了这两者的当前职责。值得注意的是,加载器不需要通过其他方法提供其 load_module() 方法的任何功能。因此,尽管有关模块的与导入相关的信息可能在不加载模块的情况下可用,但它不会以其他方式公开。

此外,与 load_module() 相关的要求对所有加载器都是通用的,并且大多以完全相同的方式实现。这意味着每个加载器都必须复制相同的样板代码。importlib.util 提供了一些有助于解决此问题的工具,但如果导入系统只负责这些职责,则会更有帮助。问题在于,这将限制 load_module() 可以轻松继续促进的自定义程度。

更重要的是,虽然查找器可以提供加载器的 load_module() 所需的信息,但它目前没有一致的方法将其传递给加载器。这是查找器和加载器之间的一个差距,此提案旨在填补这个差距。

最后,当导入系统调用查找器的 find_module() 时,查找器会利用有关模块的各种信息,这些信息在方法的上下文中很有用。目前,在方法调用之后持续保存每个模块信息的选择有限,因为它只返回加载器。此限制的常用选择是在查找器本身的某个位置存储模块到信息的映射,或将其存储在加载器上。

不幸的是,加载器不需要是特定于模块的。最重要的是,查找器可以提供的一些有用信息对所有查找器都是通用的,因此理想情况下,导入系统可以处理这些细节。这与查找器和加载器之间之前的差距相同。

作为归因于此缺陷的复杂性的一个示例,Python 3.3 中命名空间包的实现(参见PEP 420)添加了 FileFinder.find_loader(),因为 find_module() 没有好的方法来提供命名空间搜索位置。

解决此差距的答案是 ModuleSpec 对象,它包含每个模块的信息并处理加载模块时涉及的样板功能。

规范

目标是在尽可能少地更改其语义的情况下解决查找器和加载器之间的差距。虽然某些功能和信息已移至新的 ModuleSpec 类型,但它们的行为应保持不变。但是,为了清楚起见,将明确标识查找器和加载器的语义。

以下是此 PEP 描述的更改的高级摘要。更多详细信息在后面的章节中提供。

importlib.machinery.ModuleSpec(新)

在导入期间对模块的导入系统相关状态进行封装。有关更详细的说明,请参见下面的ModuleSpec部分。

  • ModuleSpec(name, loader, *, origin=None, loader_state=None, is_package=None)

属性

  • name - 模块的完全限定名称的字符串。
  • loader - 用于加载的加载器。
  • origin - 加载模块的位置名称,例如,内置模块为“builtin”,从源代码加载的模块为文件名。
  • submodule_search_locations - 如果是包,则为查找子模块的位置的字符串列表(否则为 None)。
  • loader_state - 用于加载期间的额外模块特定数据的容器。
  • cached (property) - 编译后的模块应存储在何处的字符串。
  • parent (RO-property) - 模块所属包的完全限定名称(作为子模块)(或 None)。
  • has_location (RO-property) - 一个标志,指示模块的“origin”属性是否引用一个位置。

importlib.util 添加

这些是 ModuleSpec 工厂函数,旨在为查找器提供便利。有关更多详细信息,请参见下面的工厂函数部分。

  • spec_from_file_location(name, location, *, loader=None, submodule_search_locations=None) - 从面向文件的的信息和加载器 API 构建规范。
  • spec_from_loader(name, loader, *, origin=None, is_package=None) - 通过使用加载器 API 构建具有缺失信息的规范。

其他 API 添加

  • importlib.find_spec(name, path=None, target=None) 的工作方式与 importlib.find_loader()(它所取代的)完全相同,但返回规范而不是加载器。

对于查找器

  • importlib.abc.MetaPathFinder.find_spec(name, path, target) 和 importlib.abc.PathEntryFinder.find_spec(name, target) 将返回在导入期间使用的模块规范。

对于加载器

  • importlib.abc.Loader.exec_module(module) 将在它自己的命名空间中执行模块。它取代了 importlib.abc.Loader.load_module(),接管了它的模块执行功能。
  • importlib.abc.Loader.create_module(spec)(可选)将返回用于加载的模块。

对于模块

  • 模块对象将有一个新的属性:__spec__

API 更改

  • InspectLoader.is_package() 将变为可选。

弃用

  • importlib.abc.MetaPathFinder.find_module()
  • importlib.abc.PathEntryFinder.find_module()
  • importlib.abc.PathEntryFinder.find_loader()
  • importlib.abc.Loader.load_module()
  • importlib.abc.Loader.module_repr()
  • importlib.util.set_package()
  • importlib.util.set_loader()
  • importlib.find_loader()

移除

这些是在 Python 3.4 发布之前引入的,因此可以简单地删除它们。

  • importlib.abc.Loader.init_module_attrs()
  • importlib.util.module_to_load()

其他更改

  • importlib 中的导入系统实现将更改为使用 ModuleSpec。
  • importlib.reload() 将使用 ModuleSpec。
  • 模块的与导入相关的属性(除了__spec__之外)将不再被导入系统在该模块的导入期间直接使用。但是,这不会影响在加载其他模块(例如子模块)时使用这些属性(例如__path__)。
  • 与导入相关的属性不应该再直接添加到模块中,除非由导入系统添加。
  • 模块类型的__repr__()将是对纯 Python 实现的简单包装,该实现将利用 ModuleSpec。
  • __main__模块的规范将反映相应的名称和来源。

向后兼容性

  • 如果查找器未定义 find_spec(),则从 find_module() 返回的加载器派生规范。
  • PathEntryFinder.find_loader() 仍然优先于 find_module()。
  • 如果未定义 exec_module(),则使用 Loader.load_module()。

什么不会改变?

  • import 语句的语法和语义。
  • 现有的查找器和加载器将继续正常工作。
  • 与导入相关的模块属性仍将使用相同的信息进行初始化。
  • 查找器将继续创建加载器(现在将它们存储在规范中)。
  • 如果模块定义了 Loader.load_module(),它将具有所有相同的需求,并且仍然可以直接调用。
  • 加载器将继续负责模块数据 API。
  • importlib.reload() 将继续覆盖与导入相关的属性。

职责

以下是对此 PEP 之后责任所在位置的快速分解。

查找器

  • 创建/识别可以加载模块的加载器。
  • 创建模块的规范。

加载器

  • 创建模块(可选)。
  • 执行模块。

ModuleSpec

  • 协调模块加载
  • 模块加载的样板代码,包括管理 sys.modules 和设置与导入相关的属性
  • 如果加载器没有创建模块
  • 调用 loader.exec_module(),传入要执行的模块。
  • 包含加载器执行模块所需的所有信息
  • 提供模块的 repr

现有的查找器和加载器需要做什么不同的事情?

立即?什么也没有。现状将被弃用,但将继续工作。但是,以下是查找器和加载器的作者相对于此 PEP 应该更改的内容

  • 在查找器上实现 find_spec()。
  • 如果可能,在加载器上实现 exec_module()。

importlib.util 中的 ModuleSpec 工厂函数旨在帮助转换现有的查找器。spec_from_loader() 和 spec_from_file_location() 在这方面都是简单的实用程序。

对于现有的加载器,exec_module() 应该从 load_module() 的非样板代码部分进行相对直接的转换。在某些不常见的情况下,加载器还应该实现 create_module()。

ModuleSpec 用户

ModuleSpec 对象有 3 个不同的目标受众:Python 本身、导入钩子和普通 Python 用户。

Python 将在导入机制、解释器启动和各种标准库模块中使用规范。一些模块是面向导入的,例如 pkgutil,而另一些则不是,例如 pickle 和 pydoc。在所有情况下,都将使用完整的 ModuleSpec API。

导入钩子(查找器和加载器)将以特定的方式使用规范。首先,查找器可以使用 importlib.util 中的规范工厂函数来创建规范对象。它们还可以在创建规范后直接调整规范属性。其次,查找器可以将其他信息绑定到规范(在 finder_extras 中)供加载器在模块创建/执行期间使用。最后,加载器将在创建和/或执行模块时使用规范上的属性。

Python 用户将能够检查模块的__spec__以获取有关该对象的与导入相关的信息。通常,Python 应用程序和交互式用户不会使用ModuleSpec工厂函数或任何实例方法。

加载的工作原理

以下是导入机制在加载期间执行的操作概述,调整为利用模块的规范和新的加载器 API

module = None
if spec.loader is not None and hasattr(spec.loader, 'create_module'):
    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 and spec.submodule_search_locations is not None:
    # Namespace package
    sys.modules[spec.name] = module
elif not hasattr(spec.loader, 'exec_module'):
    spec.loader.load_module(spec.name)
    # __loader__ and __package__ would be explicitly set here for
    # backwards-compatibility.
else:
    sys.modules[spec.name] = module
    try:
        spec.loader.exec_module(module)
    except BaseException:
        try:
            del sys.modules[spec.name]
        except KeyError:
            pass
        raise
module_to_return = sys.modules[spec.name]

这些步骤正是 Loader.load_module() 已经期望执行的步骤。因此,加载器将被简化,因为它们只需要实现 exec_module()。

请注意,我们必须从 sys.modules 返回模块。在加载过程中,模块可能已在 sys.modules 中替换自身。由于我们没有一个后导入钩子 API 来适应用例,所以我们必须处理它。但是,在替换情况下,我们不必担心在对象上设置与导入相关的模块属性。如果模块编写者正在执行此操作,则由他们自己负责。

重新加载的工作原理

以下是 reload() 的相应概述

_RELOADING = {}

def reload(module):
    try:
        name = module.__spec__.name
    except AttributeError:
        name = module.__name__
    spec = find_spec(name, target=module)

    if sys.modules.get(name) is not module:
        raise ImportError
    if spec in _RELOADING:
        return _RELOADING[name]
    _RELOADING[name] = module
    try:
        if spec.loader is None:
            # Namespace loader
            _init_module_attrs(spec, module)
            return module
        if spec.parent and spec.parent not in sys.modules:
            raise ImportError

        _init_module_attrs(spec, module)
        # Ignoring backwards-compatibility call to load_module()
        # for simplicity.
        spec.loader.exec_module(module)
        return sys.modules[name]
    finally:
        del _RELOADING[name]

这里的一个关键点是切换到 Loader.exec_module() 意味着加载器将不再有简单的方法来知道在执行时它是否是重新加载。在此提议之前,他们只需检查模块是否已存在于 sys.modules 中即可。现在,在加载(而不是重新加载)期间调用 exec_module() 时,导入机制已经将模块放置在 sys.modules 中。这就是 find_spec() 具有“target”参数的部分原因。

reload 的语义将基本上保持与现有语义相同[5]。此 PEP 对某些类型的延迟加载模块的影响是一个讨论点。[4]

ModuleSpec

属性

以下每个名称都是 ModuleSpec 对象上的一个属性。None 值表示“未设置”。这与模块对象形成对比,在模块对象中,属性根本不存在。大多数属性对应于模块的与导入相关的属性。以下是映射。此映射的反向描述了导入机制在调用 exec_module() 之前如何设置模块属性。

在 ModuleSpec 上 在模块上
名称 __name__
加载器 __loader__
parent __package__
来源 __file__*
cached __cached__*,**
submodule_search_locations __path__**
loader_state -
has_location -
* 仅当 spec.has_location 为 true 时才在模块上设置。
** 仅当规范属性不为 None 时才在模块上设置。

虽然 parent 和 has_location 是只读属性,但其余属性可以在创建模块规范后甚至在导入完成后替换。这允许在直接修改规范是最佳选择的不寻常情况下使用。但是,典型用法不应涉及更改模块规范的状态。

来源

“origin”是模块来源位置名称的字符串。请参见上面的origin。除了信息值外,它还用于模块的 repr。在“has_location”为 true 的规范的情况下,__file__设置为“origin”的值。对于内置模块,“origin”将设置为“built-in”。

has_location

如上文location部分所述,许多模块是“可定位的”,这意味着存在一个相应的资源,从中将加载模块,并且该资源可以用字符串描述。相反,不可定位的模块无法以这种方式加载,例如内置模块和在代码中动态创建的模块。对于这些模块,名称是访问它们的唯一方式,因此它们具有“origin”但没有“location”。

如果模块是可定位的,则“has_location”为 true。在这种情况下,规范的 origin 用作位置,并且__file__设置为 spec.origin。如果需要其他位置信息(例如 zipimport),则该信息可以存储在 spec.loader_state 中。

“has_location”可以从加载器上是否存在 load_data() 方法推断得出。

顺便说一句,并非所有可定位的模块都可缓存,但大多数都可以。

submodule_search_locations

要搜索子模块的位置字符串列表,通常是目录路径。如果模块是包,则将其设置为列表(即使是空列表)。否则为 None。

相应模块属性__path__的名称相对模糊。我们不镜像它,而是使用更明确的属性名称来使目的清晰。

loader_state

查找器可以将 loader_state 设置为任何值,以便为加载器在加载过程中使用提供其他数据。值 None 为默认值,表示没有其他数据。否则,它可以设置为任何对象,例如 dict、list 或 types.SimpleNamespace,其中包含相关的额外信息。

例如,zipimporter 可以使用它将 zip 归档文件名直接传递给加载器,而不是需要从 origin 推导出它或为每个查找操作创建自定义加载器。

loader_state 旨在供查找器和相应的加载器使用。不能保证它对任何其他用途都是稳定的资源。

工厂函数

spec_from_file_location(name, location, *, loader=None, submodule_search_locations=None)

根据面向文件的信息和加载器 API 构建规范。

  • “origin” 将设置为 location。
  • “has_location” 将设置为 True。
  • “cached” 将设置为调用 cache_from_source() 的结果。
  • 如果未传入“location”,则可以从 loader.get_filename() 推导出“origin”。
  • 如果 location 是文件名,则可以从后缀推导出“loader”。
  • 如果 location 是文件名,则可以从 loader.is_package() 和 os.path.dirname(location) 推导出“submodule_search_locations”。

spec_from_loader(name, loader, *, origin=None, is_package=None)

构建一个规范,其中缺少的信息通过使用加载器 API 填充。

  • “has_location” 可以从 loader.get_data 推导出。
  • “origin” 可以从 loader.get_filename() 推导出。
  • 如果 location 是文件名,则可以从 loader.is_package() 和 os.path.dirname(location) 推导出“submodule_search_locations”。

向后兼容性

ModuleSpec 没有任何。如果 Finder.find_module() 返回模块规范而不是加载器,情况就会不同。在这种情况下,规范必须充当将要返回的加载器。这样做相对简单,但是不必要的复杂化。它是此 PEP 早期版本的一部分。

子类化

允许使用 ModuleSpec 的子类,但没有必要。简单地设置 loader_state 或向自定义查找器或加载器添加功能可能是更好的选择,并且应该首先尝试。但是,只要子类仍然满足导入系统的要求,该类型对象作为 Finder.find_spec() 的返回值完全没问题。同样的观点适用于鸭子类型。

现有类型

模块对象

除了添加 __spec__ 外,不会更改或弃用任何与导入相关的模块属性,尽管其中一些属性可能会被弃用;任何此类弃用都可以等到 Python 4。

模块的规范不会与相应的与导入相关的属性保持同步。尽管它们可能不同,但在实践中它们通常是相同的。

一个值得注意的例外情况是使用 -m 标志将模块作为脚本运行的情况。在这种情况下,module.__spec__.name 将反映实际的模块名称,而 module.__name__ 将为 __main__

不能保证两个具有相同名称的模块的模块规范相同。同样,也不能保证对 importlib.find_spec() 的连续调用将返回相同的对象,甚至是一个等效的对象,尽管至少后者是可能的。

查找器

查找器仍然负责识别(并且通常创建)应用于加载模块的加载器。该加载器现在将存储在 find_spec() 返回的模块规范中,而不是直接返回。与当前 PEP 之前的情况一样,如果创建加载器成本很高,则可以设计该加载器以将成本推迟到以后。

MetaPathFinder.find_spec(name, path=None, target=None)

PathEntryFinder.find_spec(name, target=None)

当调用 find_spec() 时,查找器必须返回 ModuleSpec 对象。此新方法替换了 find_module() 和 find_loader()(在 PathEntryFinder 的情况下)。如果加载器没有 find_spec(),则出于向后兼容性,将使用 find_module() 和 find_loader()。

向加载器添加另一个类似的方法是一种实用性问题。find_module() 可以更改为返回规范而不是加载器。这很诱人,因为导入 API 已经遭受了太多,特别是考虑到 PathEntryFinder.find_loader() 刚刚在 Python 3.3 中添加。但是,额外的复杂性和不太明确的方法名称不值得。

find_spec() 的“target”参数

对 find_spec() 的调用可以选择包含一个“target”参数。这是随后将用作加载目标的模块对象。在正常导入(以及默认情况下)中,“target”为 None,这意味着目标模块尚未创建。在重新加载期间,传递给 reload() 的模块将作为目标传递给 find_spec()。此参数允许查找器使用比其他情况下可用的更多信息来构建模块规范。这样做在识别要使用的加载器方面特别相关。

通过 find_spec(),查找器将始终识别它将在规范中返回的加载器(或返回 None)。在识别加载器时,查找器还应确定加载器是否支持加载到目标模块中,如果传递了“target”则如此。此决定可能需要咨询加载器。

如果查找器确定加载器不支持加载到目标模块中,则它应该找到另一个加载器或引发 ImportError(完全停止模块的导入)。此确定在重新加载期间尤其重要,因为正如在 重新加载的工作方式 中所述,加载器将不再能够自己轻松识别重新加载情况。

针对“target”参数提出了两种替代方案:Loader.supports_reload() 和将“target”添加到 Loader.exec_module() 而不是 find_spec()。supports_reload() 是最初解决重新加载情况的方法。 [6] 但是,有些人反对特定于加载器、以重新加载为中心的方案。 [7]

至于 exec_module() 上的“target”,加载器可能在重新加载期间需要来自目标模块(或规范)的其他信息,而不仅仅是“此加载器是否支持重新加载此模块”,在从 load_module() 迁移后,这些信息不再可用。表上的一个提议是将类似“target”的内容添加到 exec_module()。 [8] 但是,将“target”放在 find_spec() 上更符合此 PEP 的目标。此外,它避免了对 supports_reload() 的需要。

命名空间包

当前,路径条目查找器可以从 find_loader() 返回 (None, portions) 以指示它找到了可能的命名空间包的一部分。为了达到相同的效果,find_spec() 必须返回一个规范,其中“loader”设置为 None(即未设置)并且 submodule_search_locations 设置为 find_loader() 将提供的相同部分。PathFinder 如何处理此类规范由其决定。

加载器

Loader.exec_module(module)

加载器将有一个新方法 exec_module()。它的唯一工作是“执行”模块并因此填充模块的命名空间。它不负责创建或准备模块对象,也不负责后续的任何清理工作。它没有返回值。exec_module() 将在加载和重新加载期间使用。

exec_module() 应该正确处理多次调用它的情况。对于某些类型的模块,这可能意味着在第一次调用该方法后每次都引发 ImportError。这对于重新加载尤其相关,在某些类型的模块不支持就地重新加载的情况下。

Loader.create_module(spec)

加载器也可以实现 create_module(),它将返回一个要执行的新模块。它可以返回 None 以指示应使用默认的模块创建代码。create_module() 的一个用例(尽管不典型)是提供一个作为内置模块类型的子类的模块。大多数加载器不需要实现 create_module()。

create_module() 应该正确处理为同一个规范/模块多次调用它的情况。这可能包括返回 None 或引发 ImportError。

注意

exec_module() 和 create_module() 不应设置任何与导入相关的模块属性。load_module() 执行此操作的事实是此提案旨在纠正的设计缺陷。

其他更改

PEP 420 引入了可选的 module_repr() 加载器方法,以限制模块类型 __repr__() 中的特殊情况的数量。由于此方法是 ModuleSpec 的一部分,因此它将在加载器上弃用。但是,如果它存在于加载器上,它将被独占使用。

在 Python 3.4 发布之前添加的 Loader.init_module_attr() 方法将被删除,取而代之的是 ModuleSpec 上的相同方法。

但是,即使 ModuleSpec 上找到了相同的信息,InspectLoader.is_package() 也不会被弃用。如果该信息不可用,ModuleSpec 可以使用它来填充自己的 is_package。尽管如此,它将成为可选的。

除了在加载期间执行模块外,加载器仍然直接负责提供与模块相关的数据的 API。

其他更改

  • importlib 提供的各种查找器和加载器将更新为符合此提案。
  • stdlib 中与导入相关的 API(特别是查找器和加载器)的任何其他实现或依赖项也将相应地调整到此 PEP。虽然它们应该继续工作,但任何此类遗漏的更改都应被视为 Python 3.4.x 系列的错误。
  • __main__ 模块的规范将反映解释器的启动方式。例如,使用 -m,规范的名称将是使用的模块的名称,而 __main__.__name__ 仍然是“__main__”。
  • 我们将添加 importlib.find_spec() 以镜像 importlib.find_loader()(它将被弃用)。
  • importlib.reload() 已更改为使用 ModuleSpec。
  • importlib.reload() 现在将使用每个模块的导入锁。

参考实现

参考实现可在 http://bugs.python.org/issue18864 获取。

实现说明

* 此 PEP 的实现需要意识到其对 pkgutil(和 setuptools)的影响。pkgutil 对 PEP 302 有一些基于通用函数的扩展,如果 importlib 开始在没有工具知识的情况下包装加载器,这些扩展可能会中断。

* 其他要查看的模块:runpy(和 pythonrun.c)、pickle、pydoc、inspect。

例如,pickle 应该在 __main__ 的情况下更新为查看 module.__spec__.name

PEP 中被拒绝的添加内容

此提案中有一些建议的补充,它们与提案的范围不太匹配。

没有“PathModuleSpec”作为 ModuleSpec 的子类,它将 has_location、cached 和 submodule_search_locations 分离开来。虽然这可能会使分离更清晰,但模块对象没有这种区别。ModuleSpec 将同样有效地支持这两种情况。

虽然“ModuleSpec.is_package”将是一个简单的附加属性(将 self.submodule_search_locations 不是 None 作为别名),但它延续了模块和包之间的人为(且大多是错误的)区别。

模块规范 工厂函数 可以是 ModuleSpec 上的类方法。但是,这会通过 __spec__所有模块上公开它们,这有可能不必要地混淆非高级 Python 用户。工厂函数有特定的用例,用于支持查找器作者。参见 ModuleSpec 用户

同样,可以向 ModuleSpec 添加其他一些方法,这些方法公开了导入机制对模块规范的特定使用。

  • create() - 围绕 Loader.create_module() 的包装器。
  • exec(module) - 围绕 Loader.exec_module() 的包装器。
  • load() - 已弃用的 Loader.load_module() 的类似物。

与工厂函数一样,通过 module.__spec__ 公开这些方法并不理想。即使仅作为“私有”属性公开(就像在以前版本的 PEP 中一样),它们最终也会成为一个诱人的麻烦。如果有人以后需要这些方法,我们可以在那时通过适当的 API(与 ModuleSpec 分开)公开它们,也许与 PEP 406(导入引擎)相关。

可以想象,load() 方法可以选择接受一个模块列表来与其交互,而不是 sys.modules。此外,load() 可以用来实现多版本导入。这两个都是有趣的想法,但绝对超出了本提案的范围。

其他省略的内容

  • 添加 ModuleSpec.submodules(只读属性) - 返回相对于规范的可能的子模块。
  • 添加 ModuleSpec.loaded(只读属性) - sys.module 中的模块(如果有)。
  • 添加 ModuleSpec.data - 一个包装规范加载程序的数据 API 的描述符。
  • 另见 [3]

参考文献


来源:https://github.com/python/peps/blob/main/peps/pep-0451.rst

上次修改时间:2023-10-11 12:05:51 GMT