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() 期间发生。

来源

这是一个新术语和概念。它的思想已经巧妙地存在于导入系统中,但本提案明确了这一概念。

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

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

事实上,模块 repr 已经是一个相对可靠但隐含的模块来源指示器。其他模块也通过其他方式指示其来源,如“位置”条目中描述的那样。

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

位置

这是一个新术语。然而,这个概念已经明确存在于导入系统中,与模块的 __file____path__ 属性以及其他地方的名称/术语“path”相关联。

“位置”是一种资源或“地方”,而不是一个大系统,模块从此处加载。它属于“来源”的范畴。位置的例子包括文件系统路径和 URL。位置由资源的名称标识,但不一定标识资源所属的系统。在这种情况下,加载器必须自行标识系统。

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

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

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

缓存

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

概念不变,术语也不变。然而,模块和包之间的区别大多是表面上的。包*是*模块。它们只是有一个 __path__ 属性,并且导入可能会添加绑定到子模块的属性。通常感知的差异是混淆的来源。本提案明确地弱化了在有意义的情况下包和模块之间的区别。

动机

导入系统在 Python 的生命周期中不断发展。2002年末,PEP 302 通过查找器、加载器和 sys.meta_path 引入了标准化的导入钩子。Python 3.1 引入的 importlib 模块现在公开了 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 (属性) - 编译模块应该存储位置的字符串。
  • parent (只读属性) - 模块作为子模块所属包的完全限定名(或 None)。
  • has_location (只读属性) - 一个标志,指示模块的“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 为真时才在模块上设置。
** 仅当 spec 属性不为 None 时才在模块上设置。

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

来源

“origin”是一个字符串,表示模块的来源。参见上面的origin。除了信息价值外,它还在模块的 repr 中使用。在“has_location”为真的规范情况下,__file__ 被设置为“origin”的值。对于内置模块,“origin”将设置为“built-in”。

has_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”将设置为位置。
  • “has_location”将设置为 True。
  • “cached”将设置为调用 cache_from_source() 的结果。
  • 如果未传入“location”,则“origin”可以从 loader.get_filename() 推断。
  • 如果 location 是文件名,则“loader”可以从后缀推断。
  • 如果 location 是文件名,则“submodule_search_locations”可以从 loader.is_package() 和 os.path.dirname(location) 推断。

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

通过使用加载器 API 填充缺失信息来构建规范。

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

向后兼容性

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() 的模块将作为 target 传递给 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() 后不再可用。一个摆在桌面上的提议是在 exec_module() 中添加类似“target”的东西。[8] 然而,将“target”放在 find_spec() 上更符合本 PEP 的目标。此外,它消除了对 supports_reload() 的需求。

命名空间包

目前,路径入口查找器可以从 find_loader() 返回 (None, portions) 来指示它找到了可能的命名空间包的一部分。为了实现相同的效果,find_spec() 必须返回一个规范,其中“loader”设置为 None(也即未设置),并且 submodule_search_locations 设置为与 find_loader() 提供相同的 portions。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() 应该正确处理同一个 spec/module 多次调用的情况。这可能包括返回 None 或引发 ImportError。

注意

exec_module() 和 create_module() 不应设置任何与导入相关的模块属性。load_module() 这样做是一个设计缺陷,本提案旨在纠正它。

其他更改

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

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

然而,InspectLoader.is_package() 不会被废弃,尽管相同的信息在 ModuleSpec 上也能找到。如果该信息无法从其他地方获得,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。

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

被拒绝的 PEP 补充

有一些提议的补充内容与本提案的范围不符。

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

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

模块规范 工厂函数 可以是 ModuleSpec 上的类方法。然而,这会通过 __spec__ 将它们暴露给*所有*模块,这可能会不必要地混淆非高级 Python 用户。工厂函数有一个特定的用例,即支持查找器作者。请参阅 ModuleSpec 用户

同样,ModuleSpec 还可以添加其他几个方法,这些方法暴露了导入机制对模块规范的特定使用:

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

与工厂函数一样,通过 module.__spec__ 暴露这些方法并不理想。它们最终会成为一个有吸引力的麻烦,即使只作为“私有”属性暴露(正如它们在本文档的早期版本中所做的那样)。如果有人后来发现需要这些方法,我们可以在那时通过适当的 API(独立于 ModuleSpec)暴露它们,也许与 PEP 406(导入引擎)相关。

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

其他遗漏

  • 添加 ModuleSpec.submodules (RO-property) - 返回相对于规范的可能子模块。
  • 添加 ModuleSpec.loaded (RO-property) - sys.module 中加载的模块(如果有)。
  • 添加 ModuleSpec.data - 一个描述符,包装规范加载器的数据 API。
  • 另请参阅 [3]

参考资料


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

上次修改时间:2025-02-01 08:59:27 GMT