PEP 382 – 命名空间包
- 作者:
- Martin von Löwis <martin at v.loewis.de>
- 状态:
- 已拒绝
- 类型:
- 标准跟踪
- 创建:
- 2009年4月2日
- Python 版本:
- 3.2
- 历史记录:
拒绝通知
在 2012 年美国 PyCon 大会冲刺的第一天,我们对PEP 382和PEP 402进行了长时间而富有成效的讨论。我们最终拒绝了这两个提案,但将编写一个新的 PEP 以延续 PEP 402 的精神。Martin von Löwis 撰写了一份总结:[2]。
摘要
命名空间包是一种机制,用于将单个 Python 包拆分到磁盘上的多个目录中。在当前的 Python 版本中,必须制定一个计算包的 __path__ 的算法。通过此处提出的增强功能,导入机制本身将构建构成包的目录列表。此 PEP 的实现可在[1]找到。
术语
在本 PEP 中,术语“包”指的是 Python 导入语句定义的 Python 包。术语“分发”指的是存储在 Python 包索引中、由 distutils 或 setuptools 安装的可单独安装的 Python 模块集。术语“供应商包”指的是由操作系统打包机制安装的文件组(例如,Debian 或 Redhat 包安装在 Linux 系统上)。
术语“部分”指的是单个目录中的一组文件(可能存储在 zip 文件中),这些文件有助于命名空间包。
现在的命名空间包
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 中。
基本原理
当前用于命名空间包的命令式方法导致了多种略微不兼容的提供命名空间包的机制。例如,pkgutil 支持 *.pkg
文件;setuptools 不支持。同样,setuptools 支持检查 zip 文件,并支持将其 _namespace_packages 变量添加到其中,而 pkgutil 则不支持。
此外,当前的方法会导致系统供应商出现问题。供应商包通常不得提供重叠的文件,并且尝试安装磁盘上已存在文件的供应商包将失败或导致不可预测的行为。由于供应商可能会选择打包分发版,以便它们最终都位于命名空间包的单个目录中,因此所有部分都将贡献冲突的 __init__.py 文件。
规范
此处建议使用声明式方法,而不是使用命令式机制来导入包:名称以 .pyp
(表示 Python 包)结尾的目录包含包的一部分。
导入语句扩展到计算名为 P
的包的 __path__
属性,该属性由可选的单个目录名称 P
(包含文件 __init__.py
)以及所有名为 P.pyp
的目录组成,其顺序为在父包的 __path__
(或 sys.path
)中找到的顺序。如果找到其中任何一个,则继续搜索包的其他部分。
一个目录可以同时包含 P/__init__.py
和 P.pyp
形式的包。
对导入机制没有其他更改;搜索模块(包括 __init__.py)将继续在遇到第一个模块时停止。总而言之,导入包 foo 的过程如下
- 在 sys.path 中搜索目录 foo 或 foo.pyp,或文件 foo.<ext>。如果找到文件且没有目录,则将其视为模块并导入。
- 如果找到目录 foo,则检查它是否包含 __init__.py。如果是,则记住 __init__.py 的位置。否则,跳过该目录。找到 __init__.py 后,将跳过其他名为 foo 的目录。
- 对于目录 foo 和 foo.pyp,将这些目录添加到包的 __path__ 中。
- 如果找到 __init__ 模块,则导入它,并将 __path__ 初始化为所有
.pyp
目录计算出的路径。
对导入钩子的影响
在PEP 302中定义的加载器和查找器都需要更改以支持命名空间包。不符合以下协议可能会导致包无法识别为命名空间包;不支持此协议的加载器和查找器必须在访问以下函数时引发 AttributeError。
查找器需要支持在上述算法的步骤 1 中查找 *.pth 文件。为此,用作路径钩子的查找器必须支持一个方法
finder.find_package_portion(fullname)
此方法将以与 find_module 相同的方式调用,并且必须返回一个要添加到包的 __path__
中的字符串。如果查找器未找到包的一部分,则应返回 None
。从上述调用中引发 AttributeError
将被视为不符合此 PEP,并且将忽略该异常。所有其他异常都会被报告。
查找器可以从 find_module
和 find_package_portion
中报告成功,从而允许包含 __init__.py
和同一包的一部分的包。
从 find_package_portion
返回的所有字符串以及所有 .pyp
目录的路径名称都将添加到新包的 __path__
中。
讨论
此规范的原始版本建议添加 *.pth
文件,类似于在 sys.path 上使用这些文件的方式。使用通配符标记 (*
),包可以指示整个路径是通过查看父路径并搜索正确命名的子目录来派生的。
然后人们观察到,对完整 .pth 语法的支持是不合适的,并且 .pth 文件被更改为仅是标记文件,指示目录是一个包。Peter Tröger 建议 .pth 不是一个合适的扩展名,因为与 Python 相关的扩展名都应以 .py
开头。因此,标记文件被重命名为 .pyp
。
Dinu Gherman 随后观察到,使用标记文件不是必需的,目录扩展名可以很好地用作标记。这就是此 PEP 目前建议的内容。
Phillip Eby 在比较了 Python 的包语法与其他语言中的包语法后,设计了PEP 402作为此 PEP 的替代方法。PEP 402建议完全不使用标记文件。在 2011 年 PyCon DE 大会上的讨论中,人们指出,将目录明确声明为有助于包是一个理想的特性,而不是障碍。特别是,Jython 开发人员注意到,如果不需要声明 Python 包,Jython 很容易将 Java 包误认为是 Python 包。
包可以停止填充命名空间包的 __init__.py。因此,extend_path 和 declare_namespace 变得过时。
命名空间包可以开始提供非平凡的 __init__.py 实现;为此,建议单个分发版提供一个仅包含命名空间包的 __init__.py(以及可能属于命名空间包本身的其他模块)的部分。
该机制与现有的命名空间机制大多兼容。extend_path 将调整到此规范;任何其他机制都可能导致部分被两次添加到 __path__ 中。
参考文献
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0382.rst