如何在Python中创建命名空间包?

162 投票
5 回答
84643 浏览
提问于 2025-04-15 15:40

在Python中,命名空间包让你可以把Python代码分散到多个项目中。这种做法很有用,特别是当你想把相关的库作为不同的下载包发布时。例如,如果在PYTHONPATH中有Package-1Package-2这两个目录,

Package-1/namespace/__init__.py
Package-1/namespace/module1/__init__.py
Package-2/namespace/__init__.py
Package-2/namespace/module2/__init__.py

那么最终用户可以使用import namespace.module1import namespace.module2来导入这些模块。

那么,定义一个命名空间包的最佳方法是什么,以便多个Python产品可以在这个命名空间中定义模块呢?

5 个回答

5

这一部分应该很容易理解。

简单来说,把命名空间的代码放在 __init__.py 文件里,然后更新 setup.py 文件来声明一个命名空间,这样你就可以开始了。

81

有一个标准模块,叫做 pkgutil,它可以让你把模块“添加”到指定的命名空间里。

根据你提供的目录结构:

Package-1/namespace/__init__.py
Package-1/namespace/module1/__init__.py
Package-2/namespace/__init__.py
Package-2/namespace/module2/__init__.py

你应该在 Package-1/namespace/__init__.pyPackage-2/namespace/__init__.py 这两个文件里都加上这两行代码 (*):

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

(* 这是因为 -除非你明确说明它们之间的依赖关系- 否则你不知道哪个会先被识别 - 想了解更多可以查看 PEP 420)

根据 文档 的说明:

这会把所有在 sys.path 中与这个包同名的目录的子目录添加到包的 __path__ 中。

从现在开始,你应该可以独立地分发这两个包了。

106

简而言之:

在Python 3.3中,你不需要做任何事情,只要在你的命名空间包目录中不放任何__init__.py文件,它就会正常工作。在3.3之前的版本中,建议使用pkgutil.extend_path()的方法,而不是pkg_resources.declare_namespace(),因为前者更具未来兼容性,并且已经支持隐式命名空间包。


Python 3.3引入了隐式命名空间包,具体内容可以查看PEP 420

这意味着现在通过import foo可以创建三种类型的对象:

  • 一个由foo.py文件表示的模块
  • 一个由包含__init__.py文件的目录foo表示的常规包
  • 一个由没有任何__init__.py文件的一个或多个目录foo表示的命名空间包

包也是模块,但这里我说“模块”是指“非包模块”。

首先,它会扫描sys.path来查找模块或常规包。如果找到,就停止搜索并创建和初始化该模块或包。如果没有找到模块或常规包,但找到了至少一个目录,它就会创建和初始化一个命名空间包。

模块和常规包的__file__属性会设置为它们创建时的.py文件。常规包和命名空间包的__path__属性会设置为它们创建时的目录。

当你执行import foo.bar时,首先会对foo进行上述搜索,然后如果找到了包,接着会用foo.__path__作为搜索路径来查找bar,而不是用sys.path。如果找到foo.bar,那么foofoo.bar都会被创建和初始化。

那么常规包和命名空间包是如何混合的呢?通常它们是不混合的,但旧的pkgutil显式命名空间包方法已经扩展,以包括隐式命名空间包。

如果你有一个现有的常规包,里面有这样的__init__.py文件:

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

... 旧的行为是将搜索路径上的任何其他常规包添加到它的__path__中。但在Python 3.3中,它还会添加命名空间包。

所以你可以有以下的目录结构:

├── path1
│   └── package
│       ├── __init__.py
│       └── foo.py
├── path2
│   └── package
│       └── bar.py
└── path3
    └── package
        ├── __init__.py
        └── baz.py

... 只要这两个__init__.py文件中有extend_path的相关代码(并且path1path2path3在你的sys.path中),那么import package.fooimport package.barimport package.baz都会正常工作。

pkg_resources.declare_namespace(__name__)并没有更新以包含隐式命名空间包。

撰写回答