在Python中创建命名空间包的方法

14 投票
1 回答
8627 浏览
提问于 2025-04-17 07:41

根据我在distribute中的命名空间包了解到,我可以利用命名空间包把一个大的Python包拆分成几个小的包。这真是太棒了。文档中还提到:

顺便提一下,你的项目源代码树必须包含命名空间包的 __init__.py 文件(以及任何父包的 __init__.py 文件),这在正常的Python包结构中是必须的。这些 __init__.py 文件必须包含以下内容:

__import__('pkg_resources').declare_namespace(__name__)

这段代码确保命名空间包的机制正常工作,并且当前包被注册为命名空间包。

我在想,保持目录结构和包的层级结构一致有什么好处吗?还是说这只是distribute/setuptools的命名空间包功能的技术要求?

举个例子,

我想提供一个子包 foo.bar,所以我需要建立以下的文件夹层级,并准备一个 __init__.py 文件,以便让 setup.py 正常工作,成为命名空间包:

~foo.bar/
~foo.bar/setup.py
~foo.bar/foo/__init__.py    <=    one-lined file dedicated to namespace packages
~foo.bar/foo/bar/__init__.py
~foo.bar/foo/bar/foobar.py

我对命名空间包不太熟悉,但在我看来,1) 创建foo/bar文件夹和2) 准备一个几乎只有一行的 __init__.py 文件是常规任务。它们确实提供了一些“这是一个命名空间包”的提示,但我觉得我们在 setup.py 中已经有这些信息了,对吧?

编辑:

如下面的代码块所示,我能否在我的工作目录中没有嵌套目录和一行的 __init__.py 文件的情况下创建命名空间包?也就是说,我们能否通过在 setup.py 中只放一行 namespace_packages = ['foo'] 来自动生成这些文件?

~foo.bar/
~foo.bar/setup.py
~foo.bar/src/__init__.py    <=    for bar package
~foo.bar/src/foobar.py

1 个回答

46

命名空间包在导入子包时特别有用。简单来说,当你导入 foo.bar 时,会发生以下事情:

  • 导入器会在 sys.path 中查找类似 foo 的东西。
  • 一旦找到,它会在这个发现的 foo 中查找 bar
  • 如果找不到 bar
    1. 如果 foo 是一个普通包,就会抛出一个 ImportError,表示 foo.bar 不存在。
    2. 如果 foo 是一个 命名空间 包,导入器会继续在 sys.path 中查找下一个匹配的 foo。只有当所有路径都查找完毕后,才会抛出 ImportError

这就是它的 作用,但并没有解释为什么你可能需要这样做。假设你设计了一个大型有用的库(foo),但作为其中的一部分,你还开发了一个小而非常有用的工具(foo.bar),其他 Python 程序员发现这个工具也很有用,即使他们并不需要整个库。

你可以把它们作为一个大包一起分发(就像你设计的那样),尽管大多数使用它的人只会导入这个子模块。这样用户会觉得非常不方便,因为他们必须下载整个包(200MB!),而他们其实只对一个10行的工具类感兴趣。如果你有开放许可证,可能会发现有几个人会把它分叉,结果你的工具模块就会有好几个不同的版本。

你可以重写整个库,让这个工具独立于 foo 命名空间(变成 bar 而不是 foo.bar)。这样你就可以单独分发这个工具,一些用户会很高兴,但这需要很多工作,尤其是考虑到实际上有很多用户在使用整个库,他们也需要重写程序来适应新的结构。

所以你真正想要的是一种方法,可以单独安装 foo.bar,同时在需要时又能与 foo 和谐共存。

命名空间包正好可以做到这一点,两个完全独立的 foo 包可以共存。setuptools 会识别这两个包是设计成可以并排放置的,并会礼貌地调整文件夹/文件的结构,使得两个包都在路径上,并显示为 foo,一个包含 foo.bar,另一个包含其余的 foo

你将会有两个不同的 setup.py 脚本,每个包一个。两个包中的 foo/__init__.py 都需要表明它们是命名空间包,以便导入器知道无论哪个包先被发现都要继续处理。

撰写回答