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

Python增强提案

PEP 420 – 隐式命名空间包

作者:
Eric V. Smith <eric at trueblade.com>
状态:
最终版
类型:
标准轨迹
创建:
2012年4月19日
Python版本:
3.3
发布历史:

决议:
Python-Dev 消息

目录

摘要

命名空间包是一种机制,用于将单个Python包拆分到磁盘上的多个目录中。在当前的Python版本中,必须制定一个用于计算包的__path__的算法。通过此处提出的增强功能,导入机制本身将构建构成包的目录列表。本PEP建立在之前的工作基础上,这些工作记录在PEP 382PEP 402中。这些PEP已被拒绝,转而支持本PEP。本PEP的实现位于[1]

术语

在本PEP中

  • “包”指的是Python定义的,由Python的导入语句定义的Python包。
  • “发行版”指的是存储在Python包索引中,并由distutils或setuptools安装的,可单独安装的Python模块集。
  • “供应商包”指的是由操作系统打包机制安装的文件组(例如,Debian或Redhat包安装在Linux系统上)。
  • “普通包”指的是Python 3.2及更早版本中实现的包。
  • “部分”指的是单个目录中的一组文件(可能存储在zip文件中),这些文件有助于命名空间包。
  • “旧版部分”指的是使用__path__操作来实现命名空间包的部分。

本PEP定义了一种新的包类型,“命名空间包”。

现有的命名空间包

Python目前提供pkgutil.extend_path来将包指定为命名空间包。推荐的使用方法是在

from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

包的__init__.py中。每个发行版都需要在其__init__.py中提供相同的内容,以便extend_path独立于哪个包部分首先被导入而被调用。因此,包的__init__.py实际上无法定义任何名称,因为它依赖于sys.path上包片段的顺序来确定哪个部分首先被导入。作为一个特殊功能,extend_path读取名为<packagename>.pkg的文件,这允许声明其他部分。

setuptools提供了一个名为pkg_resources.declare_namespace的类似函数,该函数以以下形式使用

import pkg_resources
pkg_resources.declare_namespace(__name__)

在部分的__init__.py中,不需要对__path__进行赋值,因为declare_namespace通过sys.modules修改包__path__。作为一个特殊功能,declare_namespace也支持zip文件,并在内部注册包名,以便setuptools对sys.path的未来添加可以正确地将其他部分添加到每个包中。

setuptools允许在发行版的setup.py中声明命名空间包,以便发行版开发者无需将魔术__path__修改放入__init__.py中。

请参阅PEP 402“问题”部分,以获取命名空间包的其他动机。请注意,PEP 402已被拒绝,但其激励用例仍然有效。

基本原理

当前命令式的方法来处理命名空间包导致了多种略微不兼容的命名空间包提供机制。例如,pkgutil支持*.pkg文件;setuptools不支持。同样,setuptools支持检查zip文件,并支持将其_namespace_packages变量添加到其他部分,而pkgutil不支持。

命名空间包旨在支持跨多个目录拆分(因此可以通过多个sys.path条目找到)。在这种配置中,多个部分是否都提供__init__.py文件并不重要,只要每个部分都正确初始化命名空间包即可。但是,Linux发行版供应商(以及其他供应商)更喜欢组合这些单独的部分并将它们全部安装到**同一个**文件系统目录中。这会产生潜在的冲突,因为这些部分现在试图在目标系统上提供**相同**的文件——许多包管理器不允许这样做。允许隐式命名空间包意味着可以完全删除提供__init__.py文件的需求,并且受影响的部分可以根据发行版的需要安装到一个公共目录中或拆分到多个目录中。

命名空间包不会受创建命名空间包时从父路径计算出的固定__path__的约束。考虑标准库encodings

  1. 假设encodings成为一个命名空间包。
  2. 它有时在解释器启动期间被导入以初始化标准IO流。
  3. 应用程序在启动后修改sys.path并希望从新的路径条目中贡献额外的编码。
  4. 尝试从在步骤3中添加的路径条目上找到的encodings部分导入编码。

如果导入系统仅限于查找在创建encodings命名空间包时存在的sys.path的值中的部分,则在步骤3中添加的其他路径将永远不会在步骤4中导入的其他部分中被搜索。此外,如果步骤2有时被跳过(由于某些运行时标志或其他条件),那么在第一次导入部分时,确实会使用在步骤3中添加的路径项。因此,本PEP要求在加载每个部分时动态计算路径条目的列表。预计导入机制将通过缓存__path__值并仅在检测到父路径已更改时刷新它们来有效地执行此操作。对于像encodings这样的顶级包,此父路径将是sys.path

规范

普通包将继续拥有一个__init__.py并将驻留在单个目录中。

命名空间包不能包含__init__.py。因此,pkgutil.extend_pathpkg_resources.declare_namespace对于创建命名空间包的目的而言已过时。将没有用于指定命名空间包的标记文件或目录。

在导入处理期间,导入机制将继续像在Python 3.2中那样迭代父路径中的每个目录。在查找名为“foo”的模块或包时,对于父路径中的每个目录

  • 如果找到<directory>/foo/__init__.py,则导入并返回一个普通包。
  • 如果没有找到,但找到<directory>/foo.{py,pyc,so,pyd},则导入并返回一个模块。扩展的确切列表因平台和是否指定-O标志而异。此处的列表具有代表性。
  • 如果没有找到,但找到<directory>/foo并且它是一个目录,则将其记录下来,并继续扫描父路径中的下一个目录。
  • 否则,继续扫描父路径中的下一个目录。

如果扫描完成而没有返回模块或包,并且至少记录了一个目录,则创建一个命名空间包。新的命名空间包

  • 具有一个__path__属性,该属性设置为在扫描期间找到并记录的路径字符串的可迭代对象。
  • 没有__file__属性。

请注意,如果执行“import foo”并且“foo”作为命名空间包被找到(使用上述规则),则“foo”会立即被创建为一个包。命名空间包的创建不会推迟到子级导入发生。

命名空间包从根本上与普通包没有区别。这只是创建包的不同方式。一旦创建了命名空间包,它与普通包之间就没有功能上的区别。

动态路径计算

导入机制的行为就像在加载每个部分之前重新计算命名空间包的__path__

出于性能原因,预计这将通过检测父路径是否已更改来实现。如果没有发生更改,则不需要重新计算__path__。实现必须确保检测到父路径内容的更改,以及检测父路径是否被替换为新的路径条目列表对象。

对导入查找器和加载器的影响

PEP 302定义了“查找器”,这些查找器被调用以搜索路径元素。这些查找器的find_module方法返回“加载器”对象或None

为了让查找器能够为命名空间包做出贡献,它必须实现一个新的 find_loader(fullname) 方法。fullname 的含义与 find_module 中的一样。find_loader 总是返回一个包含 (loader, <iterable-of-path-entries>) 的 2 元组。loader 可以为 None,在这种情况下,<iterable-of-path-entries>(可以为空)会被添加到已记录的路径条目列表中,并且路径搜索将继续。如果 loader 不为 None,则会立即使用它来加载模块或常规包。

即使返回了 loader 且它不为 None<iterable-of-path-entries> 仍然必须包含包的路径条目。这使得诸如 pkgutil.extend_path() 之类的代码能够计算它未加载的包的路径条目。

请注意,每个查找器可以有多个路径条目。这是为了支持查找器为给定的 fullname 发现多个命名空间部分的情况。许多查找器每个 find_loader 调用只支持一个命名空间包部分,在这种情况下,此可迭代对象将只包含一个字符串。

如果存在 find_loader,导入机制将调用它,否则将回退到 find_module。实现 find_module 但未实现 find_loader 的旧版查找器将无法为命名空间包贡献部分。

该规范扩展了 PEP 302 加载器,使其包含一个名为 module_repr() 的可选方法,如果存在,则用于生成模块对象的表示形式。有关更多详细信息,请参见下面的部分。

命名空间包与普通包的区别

命名空间包和常规包非常相似。不同之处在于

  • 命名空间包的部分不必全部来自相同的目录结构,甚至不必来自相同的加载器。常规包是自包含的:所有部分都位于同一目录层次结构中。
  • 命名空间包没有 __file__ 属性。
  • 命名空间包的 __path__ 属性是一个字符串的只读可迭代对象,当父路径被修改时,它会自动更新。
  • 命名空间包没有 __init__.py 模块。
  • 命名空间包的 __loader__ 属性使用不同类型的对象。

标准库中的命名空间包

标准库的部分可以实现为命名空间包,并且本 PEP 明确允许这样做。何时以及哪些标准库包将成为命名空间包不在本 PEP 的范围内。

从旧版命名空间包迁移

如上所述,在本 PEP 之前,pkgutil.extend_path() 被旧版部分用于创建命名空间包。由于所有现有命名空间包的部分都不太可能同时迁移到本 PEP,因此 extend_path() 将被修改为也识别 PEP 420 命名空间包。这将允许命名空间的一些部分是旧版部分,而其他部分则迁移到 PEP 420。这些混合命名空间包将不会具有普通命名空间包的动态路径计算功能,因为 extend_path() 在过去从未提供过此功能。

打包影响

命名空间包的多个部分可以安装到同一个目录中,也可以安装到不同的目录中。在本节中,假设有两个部分定义了“foo.bar”和“foo.baz”。“foo”本身是一个命名空间包。

如果将它们安装在同一位置,则单个目录“foo”将位于 sys.path 上的目录中。“foo”内部将有两个目录,“bar”和“baz”。如果“foo.bar”被移除(可能是由操作系统包管理器移除),则必须注意不要移除“foo/baz”或“foo”目录。请注意,在这种情况下,“foo”将是一个命名空间包(因为它缺少 __init__.py),即使它的所有部分都在同一个目录中。

请注意,“foo.bar”和“foo.baz”可以安装到同一个“foo”目录中,因为它们不会有任何共同的文件。

如果这些部分安装在不同的位置,则两个不同的“foo”目录将位于 sys.path 上的目录中。“foo/bar”将位于其中一个 sys.path 条目中,“foo/baz”将位于另一个条目中。移除“foo.bar”后,可以完全移除“foo/bar”和相应的“foo”目录。但不能移除“foo/baz”及其相应的“foo”目录。

也可以将“foo.bar”部分安装到 sys.path 上的目录中,并将“foo.baz”部分提供在一个 zip 文件中,该文件也在 sys.path 上。

示例

嵌套命名空间包

此示例使用以下目录结构

Lib/test/namespace_pkgs
    project1
        parent
            child
                one.py
    project2
        parent
            child
                two.py

在此,父级和子级都是命名空间包:它们的部分存在于不同的目录中,并且它们没有 __init__.py 文件。

在此,我们将父目录添加到 sys.path 中,并显示部分是否被正确找到

>>> import sys
>>> sys.path += ['Lib/test/namespace_pkgs/project1', 'Lib/test/namespace_pkgs/project2']
>>> import parent.child.one
>>> parent.__path__
_NamespacePath(['Lib/test/namespace_pkgs/project1/parent', 'Lib/test/namespace_pkgs/project2/parent'])
>>> parent.child.__path__
_NamespacePath(['Lib/test/namespace_pkgs/project1/parent/child', 'Lib/test/namespace_pkgs/project2/parent/child'])
>>> import parent.child.two
>>>

动态路径计算

此示例使用类似的目录结构,但添加了第三个部分

Lib/test/namespace_pkgs
    project1
        parent
            child
                one.py
    project2
        parent
            child
                two.py
    project3
        parent
            child
                three.py

我们将 project1project2 添加到 sys.path 中,然后导入 parent.child.oneparent.child.two。然后我们将 project3 添加到 sys.path 中,当导入 parent.child.three 时,project3/parent 会自动添加到 parent.__path__ 中。

# add the first two parent paths to sys.path
>>> import sys
>>> sys.path += ['Lib/test/namespace_pkgs/project1', 'Lib/test/namespace_pkgs/project2']

# parent.child.one can be imported, because project1 was added to sys.path:
>>> import parent.child.one
>>> parent.__path__
_NamespacePath(['Lib/test/namespace_pkgs/project1/parent', 'Lib/test/namespace_pkgs/project2/parent'])

# parent.child.__path__ contains project1/parent/child and project2/parent/child, but not project3/parent/child:
>>> parent.child.__path__
_NamespacePath(['Lib/test/namespace_pkgs/project1/parent/child', 'Lib/test/namespace_pkgs/project2/parent/child'])

# parent.child.two can be imported, because project2 was added to sys.path:
>>> import parent.child.two

# we cannot import parent.child.three, because project3 is not in the path:
>>> import parent.child.three
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<frozen importlib._bootstrap>", line 1286, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1250, in _find_and_load_unlocked
ImportError: No module named 'parent.child.three'

# now add project3 to sys.path:
>>> sys.path.append('Lib/test/namespace_pkgs/project3')

# and now parent.child.three can be imported:
>>> import parent.child.three

# project3/parent has been added to parent.__path__:
>>> parent.__path__
_NamespacePath(['Lib/test/namespace_pkgs/project1/parent', 'Lib/test/namespace_pkgs/project2/parent', 'Lib/test/namespace_pkgs/project3/parent'])

# and project3/parent/child has been added to parent.child.__path__
>>> parent.child.__path__
_NamespacePath(['Lib/test/namespace_pkgs/project1/parent/child', 'Lib/test/namespace_pkgs/project2/parent/child', 'Lib/test/namespace_pkgs/project3/parent/child'])
>>>

讨论

在 2012 年的 PyCon 上,我们讨论了命名空间包,其中 PEP 382PEP 402 被否决,取而代之的是本 PEP [3]

我们无意移除对常规包的支持。如果开发人员知道她的包永远不会成为命名空间包的一部分,那么将其作为常规包(带有 __init__.py)会带来性能优势。常规包的创建和加载可以在它位于路径上的时候立即进行。对于命名空间包,必须扫描路径中的所有条目才能创建包。

请注意,对于缺少 __init__.py 文件的目录,将不再引发 ImportWarning。此类目录现在将作为命名空间包导入,而在以前的 Python 版本中,会引发 ImportWarning。

Alyssa (Nick) Coghlan 列出了一系列她对该提案的异议 [4]。它们是

  1. 隐式包目录违反了 Python 之禅。
  2. 隐式包目录带来了棘手的向后兼容性挑战。
  3. 隐式包目录给文件系统布局带来了歧义。
  4. 隐式包目录将永久地将当前对新手不友好的行为固定在 __main__ 中。

Alyssa 后来详细回复了她自己的异议 [5],这里对其进行了总结

  1. 本 PEP 的实用性胜过其他提案和现状。
  2. 只要正确记录,轻微的向后兼容性问题是可以接受的。
  3. 这将在 PEP 395 中解决。
  4. 这将在 PEP 395 中解决。

在标准库中包含命名空间包的动机来自 Martin v. Löwis,他希望 encodings 包成为命名空间包 [6]。虽然本 PEP 允许标准库包成为命名空间,但它推迟了关于 encodings 的决定。

find_modulefind_loader

本 PEP 的早期草案指定了对 find_module 方法的更改以支持命名空间包。它将在发现命名空间包部分的情况下修改为返回一个字符串。

但是,这导致标准库外部的现有代码(调用 find_module)出现问题。由于此代码不会与本 PEP 所需的更改同步升级,因此当它从 find_module 接收意外的返回值时,它将失败。由于这种不兼容性,本 PEP 现在指定,想要提供命名空间部分的查找器必须实现上面描述的 find_loader 方法。

[7] 中给出了每个 find_loader 调用支持多个部分的用例。

动态路径计算

Guido 对自动动态路径计算是一个不必要的特性表示担忧 [8]。在该主题的后续讨论中,PJ Eby 和 Alyssa Coghlan 阐述了为什么动态计算可以最大程度地减少 Python 用户的意外情况。该讨论的结论已包含在本 PEP 的原理部分。

本 PEP 的早期版本要求只有当父路径对象被就地修改时,动态路径计算才能生效。也就是说,这将起作用

sys.path.append('new-dir')

但这不会

sys.path = sys.path + ['new-dir']

在同一个主题 [8] 中,有人指出此限制不是必需的。如果通过名称而不是通过持有对它的引用来查找父路径,则对如何修改或替换父路径没有限制。对于顶级命名空间包,查找将是名为 "sys" 的模块,然后是其属性 "path"。对于嵌套在包 foo 中的命名空间包,查找将是名为 "foo" 的模块,然后是其属性 "__path__"

模块表示

之前,模块的表示形式是根据对模块的__file__属性的假设硬编码的。如果此属性存在且为字符串,则假定它是一个文件系统路径,并且模块对象的表示形式将包含此路径在其值中。唯一的例外是PEP 302将缺少的__file__属性保留给内置模块,在CPython中,此假设被烘焙到模块对象的实现中。由于此限制,一些模块包含人为的__file__值,这些值不反映文件系统路径,并且可能在以后导致意外问题(例如,在非路径__file__上使用os.path.join()将返回乱码)。

本PEP放宽了此约束,并将__file__的设置留给生成模块的加载器。如果没有任何文件系统路径适用,加载器可以选择不设置__file__。如果有用,加载器也可以在模块上设置其他保留属性。这意味着确定模块来源的明确方法是检查其__loader__属性。

例如,本PEP中描述的命名空间包将没有__file__属性,因为不存在相应的文件。为了在这些模块的表示形式中提供灵活性和描述性,在PEP 302加载器中添加了一个新的可选协议。加载器可以实现一个module_repr()方法,该方法接受一个参数,即模块对象。此方法应返回用作模块表示形式的字符串。生成模块表示形式的规则现在已标准化为

  • 如果模块具有__loader__并且该加载器具有module_repr()方法,则使用模块对象作为单个参数调用它。返回值用作模块的表示形式。
  • 如果module_repr()中发生异常,则捕获并丢弃该异常,并且模块表示形式的计算将继续进行,就像module_repr()不存在一样。
  • 如果模块具有__file__属性,则将其用作模块表示形式的一部分。
  • 如果模块没有__file__但具有__loader__,则使用加载器的表示形式作为模块表示形式的一部分。
  • 否则,只需在表示形式中使用模块的__name__

以下是一个代码片段,展示了如何根据其加载器计算命名空间模块的表示形式

class NamespaceLoader:
    @classmethod
    def module_repr(cls, module):
        return "<module '{}' (namespace)>".format(module.__name__)

内置模块的表示形式不再需要硬编码,而是也来自其加载器

class BuiltinImporter:
    @classmethod
    def module_repr(cls, module):
        return "<module '{}' (built-in)>".format(module.__name__)

以下是一些不同类型的模块的不同表示形式示例,这些模块具有相关属性的不同集合

>>> import email
>>> email
<module 'email' from '/home/barry/projects/python/pep-420/Lib/email/__init__.py'>
>>> m = type(email)('foo')
>>> m
<module 'foo'>
>>> m.__file__ = 'zippy:/de/do/dah'
>>> m
<module 'foo' from 'zippy:/de/do/dah'>
>>> class Loader: pass
...
>>> m.__loader__ = Loader
>>> del m.__file__
>>> m
<module 'foo' (<class '__main__.Loader'>)>
>>> class NewLoader:
...   @classmethod
...   def module_repr(cls, module):
...      return '<mystery module!>'
...
>>> m.__loader__ = NewLoader
>>> m
<mystery module!>
>>>

参考文献


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

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