打包命名空间包#

命名空间包允许你将单个 中的子包和模块拆分成多个单独的 分发包(本文档中称为分发,以避免歧义)。例如,如果你有以下包结构

mynamespace/
    __init__.py
    subpackage_a/
        __init__.py
        ...
    subpackage_b/
        __init__.py
        ...
    module_b.py
pyproject.toml

并且你在代码中像这样使用此包

from mynamespace import subpackage_a
from mynamespace import subpackage_b

那么你可以将这些子包拆分成两个单独的分发

mynamespace-subpackage-a/
    pyproject.toml
    src/
        mynamespace/
            subpackage_a/
                __init__.py

mynamespace-subpackage-b/
    pyproject.toml
    src/
        mynamespace/
            subpackage_b/
                __init__.py
            module_b.py

现在,每个子包都可以单独安装、使用和进行版本控制。

命名空间包对于大量松散相关的包(例如,来自单一公司的多个产品的庞大客户端库语料库)很有用。但是,命名空间包有一些警告,并不适用于所有情况。一个简单的替代方法是在你的所有分发上使用前缀,例如 import mynamespace_subpackage_a(你甚至可以使用 import mynamespace_subpackage_a as subpackage_a 来保持导入对象的简短)。

创建命名空间包#

目前有两种不同的创建命名空间包的方法,其中后者不推荐使用

  1. 使用 本机命名空间包。这种类型的命名空间包在 PEP 420 中定义,可在 Python 3.3 及更高版本中使用。如果你命名空间中的包只需要支持 Python 3 和通过 pip 安装,则建议使用此方法。

  2. 使用 旧版命名空间包。这包括 pkgutil 样式命名空间包pkg_resources 样式命名空间包

本机命名空间包#

Python 3.3 从 PEP 420 添加了隐式命名空间包。创建本机命名空间包所需的所有操作就是从命名空间包目录中省略 __init__.py。一个示例文件结构(遵循 src-layout

mynamespace-subpackage-a/
    pyproject.toml # AND/OR setup.py, setup.cfg
    src/
        mynamespace/ # namespace package
            # No __init__.py here.
            subpackage_a/
                # Regular import packages have an __init__.py.
                __init__.py
                module.py

非常重要的是,使用命名空间包的每个分发都省略 __init__.py 或使用 pkgutil 样式的 __init__.py。如果任何分发没有这样做,它将导致命名空间逻辑失败,并且其他子包将不可导入。

src-layout 目录结构允许大多数 构建后端 自动发现包。有关更多信息,请参阅 src 布局与平面布局。但是,如果你想自己管理包的排除或包含,可以在顶级 pyproject.toml 中配置此项

[build-system]
...

[tool.setuptools.packages.find]
where = ["src/"]
include = ["mynamespace.subpackage_a"]

[project]
name = "mynamespace-subpackage-a"
...

可以使用 setup.cfg 完成相同操作

[options]
package_dir =
    =src
packages = find_namespace:

[options.packages.find]
where = src

setup.py

from setuptools import setup, find_namespace_packages

setup(
    name='mynamespace-subpackage-a',
    ...
    packages=find_namespace_packages(where='src/', include=['mynamespace.subpackage_a']),
    package_dir={'': 'src'},
)

Setuptools 默认情况下将搜索目录结构以查找隐式命名空间包。

可以在 本机命名空间包示例项目 中找到两个本机命名空间包的完整工作示例。

注意

由于原生和 pkgutil 风格的命名空间包在很大程度上兼容,因此您可以在仅支持 Python 3 的发行版中使用原生命名空间包,在需要支持 Python 2 和 3 的发行版中使用 pkgutil 风格的命名空间包。

旧版命名空间包#

PEP 420 之前用于创建命名空间包的这两种方法现已视为过时,除非您需要与已使用此方法的包兼容,否则不应使用。此外,pkg_resources 已弃用。

要迁移现有包,必须同时迁移共享命名空间的所有包。

警告

虽然原生命名空间包和 pkgutil 风格的命名空间包在很大程度上兼容,但 pkg_resources 风格的命名空间包与其他方法不兼容。不建议在为同一命名空间提供包的不同发行版中使用不同的方法。

pkgutil 风格的命名空间包#

Python 2.3 引入了 pkgutil 模块和 pkgutil.extend_path() 函数。这可用于声明需要同时兼容 Python 2.3+ 和 Python 3 的命名空间包。这是获得最高兼容性的推荐方法。

要创建 pkgutil 风格的命名空间包,您需要为命名空间包提供 __init__.py 文件

mynamespace-subpackage-a/
    src/
        pyproject.toml # AND/OR setup.cfg, setup.py
        mynamespace/
            __init__.py  # Namespace package __init__.py
            subpackage_a/
                __init__.py  # Regular package __init__.py
                module.py

命名空间包的 __init__.py 文件需要包含以下内容

__path__ = __import__('pkgutil').extend_path(__path__, __name__)

每个使用命名空间包的发行版都必须包含这样的 __init__.py。如果任何发行版不包含,它将导致命名空间逻辑失败,并且其他子包将无法导入。__init__.py 中的任何其他代码都将无法访问。

可以在 pkgutil 命名空间示例项目 中找到两个 pkgutil 风格的命名空间包的完整工作示例。

pkg_resources 风格的命名空间包#

Setuptools 提供了 pkg_resources.declare_namespace 函数和 namespace_packages 参数,用于 setup()。这些可以一起用于声明命名空间包。虽然此方法不再推荐,但它广泛存在于大多数现有命名空间包中。如果您正在使用此方法在现有命名空间包中创建新发行版,则建议继续使用此方法,因为不同的方法不具有交叉兼容性,并且不建议尝试迁移现有包。

要创建 pkg_resources 风格的命名空间包,您需要为命名空间包提供 __init__.py 文件

mynamespace-subpackage-a/
    src/
        pyproject.toml # AND/OR setup.cfg, setup.py
        mynamespace/
            __init__.py  # Namespace package __init__.py
            subpackage_a/
                __init__.py  # Regular package __init__.py
                module.py

命名空间包的 __init__.py 文件需要包含以下内容

__import__('pkg_resources').declare_namespace(__name__)

每个使用命名空间包的发行版都必须包含这样的 __init__.py。如果任何发行版不包含,它将导致命名空间逻辑失败,并且其他子包将无法导入。__init__.py 中的任何其他代码都将无法访问。

注意

一些较旧的建议在命名空间包 __init__.py 中建议以下内容

try:
    __import__('pkg_resources').declare_namespace(__name__)
except ImportError:
    __path__ = __import__('pkgutil').extend_path(__path__, __name__)

这样做背后的想法是,在极少数情况下 setuptools 不可用的情况下,包会回退到 pkgutil 风格的包。这是不建议的,因为 pkgutil 和 pkg_resources 风格的命名空间包不具有交叉兼容性。如果 setuptools 的存在是一个问题,那么包应该通过 install_requires 显式依赖于 setuptools。

最后,每个发行版都必须在 setup.py 中为 setup() 提供 namespace_packages 参数。例如

from setuptools import find_packages, setup

setup(
    name='mynamespace-subpackage-a',
    ...
    packages=find_packages()
    namespace_packages=['mynamespace']
)

可以在 pkg_resources 命名空间示例项目 中找到两个 pkg_resources 风格的命名空间包的完整工作示例。