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

160

在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产品可以在该命名空间中定义模块?


8
在我看来,module1和module2实际上是子包而不是模块。据我了解,模块基本上是一个单独的文件。也许使用subpkg1和subpkg2作为名称会更合适? - Alan
5个回答

105

TL;DR:

在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__)尚未更新以包括隐式命名空间包。


4
关于setuptools呢?我是否必须使用namespace_packages选项?还有__import__('pkg_resources').declare_namespace(__name__)这件事情呢? - kawing-chiu
6
我应该在setup.py中添加namespace_packages = ['package']吗? - Laurent LAPORTE
2
@clacke:使用namespace_packages=['package'],setup.py将在EGG-INFO中添加一个namespace_packages.txt。仍然不知道影响是什么... - Laurent LAPORTE
1
@kawing-chiu pkg_resources.declare_namespace 相对于 pkgutil.extend_path 的好处在于它将继续监视 sys.path。这样,如果在首次加载命名空间中的包之后向 sys.path 添加了新项,则该新路径项中的命名空间中的包仍然能够被加载。 (使用 __import__('pkg_resources') 而不是 import pkg_resources 的好处在于您不会将 pkg_resources 暴露为 my_namespace_pkg.pkg_resources。) - Arthur Tacca
1
@clacke,它不是那样工作的(但它具有与其相同的效果)。它维护了一个全局列表,其中包含使用该函数创建的所有软件包命名空间,并监视sys.path。当sys.path更改时,它会检查是否影响任何命名空间的__path__,如果影响,则更新这些__path__属性。 - Arthur Tacca
显示剩余6条评论

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

正如文档所述:

这将向包的__path__添加sys.path中以该包命名的目录的所有子目录。

从现在开始,您应该能够独立地分发这两个软件包。


19
使用该方法与使用 import('pkg_resources').declare_namespace(name) 方法相比,有哪些优缺点? - joeforker
15
首先,在这种情况下,__import__ 被认为是不良风格,因为它可以轻松地替换为普通的 import 语句。 更重要的是,pkg_resources 是一个非标准库。它随 setuptools 一起提供,所以这不是一个问题。快速搜索表明,pkgutil 是在版本 2.5 中引入的,而 pkg_resources 的推出要早于它。尽管如此,pkgutil 是一个正式认可的解决方案。实际上,pkg_resources 的纳入在 PEP 365 中被拒绝了。 - Mike Hordecki
8
这两行代码应该放在Package-1/namespace/__init__.pyPackage-2/namespace/__init__.py两个文件中,假设我们不知道哪个包的目录被列在首位。请注意,翻译时不能改变原意。 - Bula
3
是的,这正是重点,如果您知道哪个是第一个,那没问题,但真正的问题是您能保证它在任何情况下都是第一个吗?也就是说,对于其他用户呢? - Bula
3
@Bula:你说得对。来自PEP 420的内容是:“每个发行版都需要在它的__init__.py中提供相同的内容,这样extend_path就会在导入包的任何部分时被调用。” - Søren Løvborg
显示剩余10条评论

5

18
你应该始终引用链接的相关部分,以防相关链接失效。 - Tinned_Tuna

2
这是一个老问题,但最近有人在我的博客上评论说我的命名空间包的帖子仍然相关,所以我想在这里链接它,因为它提供了一个实际的例子来说明如何使其工作:https://web.archive.org/web/20150425043954/http://cdent.tumblr.com/post/216241761/python-namespace-packages-for-tiddlyweb
那个链接指向这篇文章,介绍了主要的内容: http://www.siafoo.net/article/77#multiple-distributions-one-virtual-package__import__("pkg_resources").declare_namespace(__name__)技巧几乎驱动了TiddlyWeb中插件的管理,并且到目前为止似乎还可以。

siafoo的链接已经失效了,这里是一个存档副本的链接: https://web.archive.org/web/20200926015931/http://www.siafoo.net/article/77 - JCowfer

-11

你的Python命名空间概念是颠倒的,Python不允许将包放入模块中。包含模块的是包而不是相反。

Python包只是一个包含__init__.py文件的文件夹。模块是包中(或直接在PYTHONPATH上)具有.py扩展名的任何其他文件。因此,在您的示例中,您有两个包但未定义任何模块。如果您认为包是文件系统文件夹,模块是文件,那么您就会明白为什么包含模块而不是相反。

因此,在您的示例中,假设Package-1和Package-2是您放在Python路径上的文件夹,则可以有以下内容:

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

现在您有一个包 namespace,其中包含两个模块 module1module2。除非您有充分的理由,否则您应该将这些模块放在文件夹中,并且只将该文件夹添加到 Python 路径中,如下所示:

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

我所说的是像 zope.x 这样的东西,其中一堆相关的包被作为单独的下载发布。 - joeforker
好的,但你想要实现什么效果呢?如果包含相关包的文件夹都在PYTHONPATH上,Python解释器将会自动找到它们,无需额外努力。 - Tendayi Mawushe
5
如果您将Package-1和Package-2都添加到PYTHONPATH中,那么Python只会看到Package-1/namespace/。 - Søren Løvborg

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接