python 的包并不一定需要一个 __init__.py 文件。
1、命名空间包
不包含 __init__.py 文件的包叫 命名空间包,这种包有几个差异的地方。假如文件的组织结构如下,每一个 py 文件都只简单的包含一句 print(__name__) 。包路径下不包含 __init__.py 文件:
import sys
sys.path.append(r"D:\foo\spam_1")
sys.path.append(r"D:\foo\spam_2")
import bar
print("1、", dir(bar))
import bar.x
import bar.y
import bar.z
print("2、", dir(bar))
print("3、", bar, bar.__path__, type(bar.__path__), "len:", len(bar.__path__))
print("4、", bar.__file__, type(bar.__file__))
由于 bar 输出如下:
代码中首先将两个不同路径下的 bar 包加到 python 解释器的搜索路径下。
第 1、2两条输出说明,包下的模块只有在导入之后,才会存在包的属性当中。
第 3 的结果用来区分普通意义上包含 __init__.py 的包 --- namespace 标志。命名空间包的路径包含多个路径。
第 4 条输出,命名空间包的 __file__ 属性为None (此处结果的python 运行环境版本为 3.7.3,python cookbook 第3版第 10.5
节的 Discussion 部分提到命名空间包不具有 __file__ 属性,与这里不符,未求证书中结果的 python 版本)。
命名空间包的一个用处就是将位于不同物理路径下的包合并到一个逻辑命名空间当中。当然所有的这些路径都必须是命名空间包,即不能包含 __init__.py 文件。
如果其中一个包不是命名空间包,那么导入的命名空间包处将报错。
在 D:\foo\spam_1\bar 目录下添加空的 __init__.py 文件,即 y.py 和 z.py 所在的路径不是命名空间包,运行将报错:
1、 ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__']
Traceback (most recent call last):
bar.x
File "G:/code_project/python/pro_expert/import_fragment.py", line 11, in <module>
import bar.y
ModuleNotFoundError: No module named 'bar.y'
当然,如果在 D:\foo\spam_1\bar 目录下添加空的 __init__.py 文件,即 x.py 文件所在的路径不是命名空间包,运行将报错:
ModuleNotFoundError: No module named 'bar.x'
如果两个目录都是非命名空间包,即普通意义上包含 __init__.py 包,那么也是将报错。这根 import 的搜索顺序有关,由于 spam_1 在搜索路径中优先被找到,所以 import bar 导出的将是 spam_1/bar,而此 bar 包下无 y 和 z模块,所以会报模块 bar.y 不存在的错。
2、普通包
普通包就是通常意义上包含 __init__.py 文件的包,即非命名空间包。
在 D:\foo\spam_2\bar 目录下添加空的 __init__.py 文件。
代码如下:
import sys
#sys.path.append(r"D:\foo\spam_1")
sys.path.append(r"D:\foo\spam_2")
import bar
print("1、", dir(bar))
#import bar.x
import bar.y
import bar.z
print("2、", dir(bar))
print("3、", bar, bar.__path__, type(bar.__path__))
print("4、", bar.__file__, type(bar.__file__))
输出如下:
第 3 条输出,普通包的标志是 from __init__.py 文件(上边提到命名空间包的标志是 namespace)。
第 4 条输出,普通包的 __file__ 属性值即 __init__.py 文件名字符串。
第 3 条输出中看到,普通包的 __path__ 属性是一个列表,可以通过这个属性 hack 普通包拥有命名空间包一样的多个物理路径的功能。
代码末尾添加如下部分:
bar.__path__.append(r"D:\foo\spam_1\bar")
print("5、", bar, bar.__path__, type(bar.__path__), "len:", len(bar.__path__))
import bar.x
print("6、", dir(bar))
输出如下:
可以看到普通包也有了多个物理路径。