在 Python3 中,即使一个文件夹中没有定义 __init__.py,也是可以被导入的,只不过它不是以 Python 包的形式导入,而是以命名空间包 (Namespace package) 的形式被导入,而这一特性是在 Python 3.3 被引入的。

比如下面的目录结构:

1
2
3
4
5
6
7
.
├── configs
│   └── config.py
├── depends
├── tools
└── __init__.py
│   └── tool.py

如果要在 tools/tool.py 导入 configs/config.py 中的内容,在 Python 3(3.3及之后) 中就可以直接使用 from configs.config import * 来导入,Python 2 中就不可以,如果一定要这样做,就需要给 configs 文件夹下加入 __init__.py 才可以。

但是所谓的命名空间包提出的本意却不是说为了导入没有 __init__.py 的文件夹的 Python 模块,而是利用命名空间包这个技术来导入目录分散的代码。

在不同的目录里有相同的命名空间,但是要删去用来将组件联合起来的 __init__.py 文件。假设你有Python代码的两个不同的目录如下:

1
2
3
4
5
6
7
foo-package/
spam/
blah.py
bar-package/
spam/
grok.py

在这2个目录里,都有着共同的命名空间spam。在任何一个目录里都没有 __init__.py 文件。这时候再导入这两个包的时候:

1
2
3
4
import sys
sys.path.extend(['foo-package', 'bar-package'])
import spam.blah
import spam.grok

两个不同的包目录被合并到一起,你可以导入spam.blah和spam.grok,并且它们能够工作。

在这里工作的机制被称为命名空间包的一个特征。从本质上讲,命名空间包是一种特殊的封装设计,为合并不同的目录的代码到一个共同的命名空间。

命名空间包的关键是确保顶级目录中没有 __init__.py 文件来作为共同的命名空间。缺失 __init__.py 文件使得在导入包的时候会发生有趣的事情:这并没有产生错误,解释器创建了一个由所有包含匹配包名的目录组成的列表。特殊的包命名空间模块被创建,只读的目录列表副本被存储在其 __path__ 变量中。

1
2
3
4
>>> import spam
>>> spam.__path__
_NamespacePath(['foo-package/spam', 'bar-package/spam'])
>>>

上面 configs 的例子其 __path__ 变量如下:

1
2
3
>>> import configs
>>> configs.__path__
_NamespacePath(['/project/configs'])

在定位包的子组件时,目录 __path__ 将被用到(例如, 当导入 spam.grok 或者 spam.blah 的时候或者 configs.config).

一个包是否被作为一个包命名空间的主要方法是检查其 __file__ 属性。如果没有,那包是个命名空间。这也可以由其字符表现形式中的 namespace 这个词体现出来。

1
2
3
4
5
6
7
8
9
>>> configs.__file__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: module 'configs' has no attribute '__file__'
>>> import other
>>> other.__file__
'/other/__init__.py'
>>> other
<module 'other' from '/other/__init__.py'>

参考