Python命名空间包

提到命名空间,其实包含 2 层含义,一种是语言上下文的命名空间,其实在编写 Python 程序时,无时无刻都在使用命名空间,比如模块使用的全局命名空间、函数或方法调用的本地命名空间等;另一种命名空间可以在包的层面提供,称为命名空间包

简单的理解,命名空间包是对相关的包或模块进行分组的一种方法,通常是一个被忽略的功能,它对于在较大的项目中构建打包生态系统非常有用。

如果你的应用组件的开发、打包和版本化都是独立的,但仍然希望从同一个命名空间访问它们,那么命名空间包特别有用,它有利于明确每个包所属的组织或项目。

例如,假设有一个 Acme 公司,该公司中使用共同的 acme 命名空间,同时创建通用的 acme 命名空间包作为该组织的其他包的容器。如果 Acme 公司中的某人想要向这个命名空间贡献一个与 SQL 相关的库,那么他需要在 acme 中注册自己新的 acme.sql 包,整个文件结构如下所示:
$tree acme/
acme/
├───acme
│      ├───__init__.py
│      └───sql
│              └──__init__.py
└───setup.py

2 directories, 3 files
在此基础上,如果想添加一个新的子包,例如添加 templating,则需要将其包含在 acme 的源代码树中,如下所示:
$tree acme/
acme/
├───acme
│      ├───__init__.py
│      ├───sql
│      │      └──__init__.py
│      └───templating
│              └──__init__.py
└───setup.py

3 directories, 4 files
仔细观察就会发现,采用这种方式几乎不可能单独开发 acme.sql 和 acme.templating。且 setup.py 脚本还必须指定每个子包的所有依赖,所以不可能(至少非常困难)选择性地安装 acme 中的部分组件。此外,如果某些子包的需求文件有冲突,是一个无法解决的问题。

通过利用命名空间包,我们可以单独保存每个子包的源代码树,如下所示:
$tree acme.sql/
acme.sql/
├───acme
│      └───sql
│              └──__init__.py
└───setup.py

2 directories, 2 files

$tree acme.templating/
acme.templating/
├───acme
│      └───templating
│                 └──__init__.py
└───setup.py

2 directories, 2 files
由此,就可以在 PyPI 或者使用的任何包索引中单独注册它们,用户还可以从 acme 命名空间中选择想要安装的子包,而无需安装通用的 acme 包,执行代码如下:

$pip install acme.sql acme.templating

注意,独立的源代码树不足以在 Python 中创建命名空间包,如果不想让包之间相互覆盖,就需要做一些额外的工作,此外,正确的处理方式也会随着 Python 版本的不同而有所不同。

隐式命名空间包

如果你只使用 Python 3.x,也只面向 Python 3.x 的用户,则可以使用 PEP 420 引入的定义命令空间的新方法,即隐式命名空间包。它是标准路径的一部分,并从 Python 3.3 版本开始成为语言官方内容的一部分。

简单来说,对于每一个包含 Python 包或模块(也包括命名空间包)的目录来说,如果其不包含 __init__().py 文件,那么它就被看做是命名空间包。

例如,前面所说的 acme 在 Python 3.3 以及更高版本中,就是一个命名空间包。使用安装工具的最小 setup.py 脚本文件如下所示:
from setuptools import setup

setup(
    name = 'acme.templating',
    packages = ['acme.templating'],
)
但是,直到发表本节时,setuptools.find_packages() 还不支持 PEP 420,但这在未来很可能会改变。此外,要想实现命名空间包的简单继承,显示地定义包列表是值得的。

以前Python版本中的命名空间包

Python 3.3 之前的版本中,虽无法使用 PEP 420 布局中的命名空间包,但仍可以使用它。旧版 Python 中,有几种方法可以将包定义成命名空间。

最简单的方法就是为每个组件创建一个文件结构,类似于没有命名空间包的普通包布局,并将所有事情都留给 setuptools。

因此,acme.sql 和 acme.templating 的布局示例可能如下所示:
$tree acme.sql/
acme.sql/
├───acme
│      ├──__init__.py
│      └───sql
│              └──__init__.py
└───setup.py

2 directories, 3 files

$tree acme.templating/
acme.templating/
├───acme
│      ├──__init__.py
│      └───templating
│                └──__init__.py
└───setup.py

2 directories, 3 files
注意,acme.sql 和 acme.templating 都有一个额外的源代码文件 acme/__init__.py,这个文件必须是空的。

如果我们提供 acme 作为 setuptools.setup() 函数 namespace_package 关键字参数的值,那么将会创建如下的 acme 命名空间包:
from setuptools import setup

setup(
    name = 'acme.templating',
    packages = ['acme.templating'],
    namespace_package = ['acme'],
)
当然,最简单的方法不一定是最好的,为了注册一个新的命名空间,setuptools 将会在 __init__.py 文件中调用 pkg_resources.declare_namespace() 函数,即便 __init__.py 文件是空的也会调用。

无论如何,正如官方文档所说,你自己负责在 __init__.py 文件中声明命名空间,并且未来可能会删除 setuptools 的这个隐式行为。为了保证安全,也为了未来依然可用(future-proof),需要将下面这行代码添加到 acme/__init__.py 文件中:

__import__('pkg_resources').declare_namespace(__name__)