Python Package __init__解析¶
发布于:2018-10-21 | 分类:python/vba/cpp
单个Python文件可以作为一个Module被导入重用,多个Module可以组织为Package。Package下一般都有一个__init__.py文件,而且内容可能啥也没有。本篇即解释此__init__.py文件的使用。
文件结构¶
以下文件结构作为本篇试验测试依据:
+-----------------------------+ +---------------------------------+
| + Package | |# Package/Sub_Package/module_a.py|
| | | | |x = 1 |
| | + Sub_Package | +---------------------------------+
| | | | |
| | | + module_a.py | +---------------------------------+
| | | + module_b.py | |# Package/Sub_Package/module_b.py|
| | | + __init__.py | |from . import module_a |
| | | | |x = module_a.x + 1 |
| | + module_c.py | +---------------+-----------------+
| | | |
| | + __init__.py | +---------------------+
| + test.py | |# Package/module_c.py|
| | |x = 3 |
+-----------------------------+ +---------------------+
其中,
test.py为测试文件,Package为待测试的包Package包含模块module_c.py和子包Sub_Package,以及该包的__init__.py文件Sub_Package包含两个模块module_a.py和module_b.py,以及该包的__init__.py文件Sub_Package中module_a模块被module_b导入使用
以下基于三种不同的导包方式,示例__init__.py的作用。
from Package import module¶
此为精确导入的形式——明确指定导入模块的名称。这种情况对__init__.py没有特殊要求,可以为空甚至不需要该文件。其作用仅仅是表明Package文件夹管理的是一个包。
# test.py
from Package import module_c
print(module_c.x) # 3
# 导入子包中的模块
from Package.Sub_Package import module_a
print(module_a.x) # 1
from Package import *¶
如果依旧保持__init__.py为空,则下面代码无法正确导入module_c模块。
# test.py
from Package import *
print(module_c.x) # name 'module_c' is not defined
对于这种模糊导入的方式,需要在__init__.py中定义变量__all__来指定from Package import *时所包含的内容。补充如下代码后,即可使用上一段代码正确引入module_c。
# Package/__init__.py
__all__ = ['module_c']
__init__.py文件会在导入包的时候执行,因此可以在其中进行一些初始化的操作,例如本例中初始化一个变量x:
# Package/__init__.py
x = 4
__all__ = ['module_c', 'x']
# test.py
from Package import *
print(module_c.x) # 3
print(x) # 4
import Package¶
实际上,import Package不会导入任何模块或者子包,以上例中的Package/__init__.py为当前状态,则有:
# test.py
import Package
print(Package.module_c.x) # module 'Package' has no attribute 'module_c'
print(Package.x) # 4
由于未导入任何模块,故Package.module_c出错;由于导包的时候执行了__init__.py,即初始化了变量x,故Package.x正确。
解决方法:利用__init__.py文件在导入包时被执行的特点,在__init__.py中显式导入子模块/子包。
更新后的文件如下图所示,则上一段代码可以正确执行了。
# Package/__init__.py
from . import module_c # 显式导入子模块
from . import Sub_Package # 显式导入子包
x = 4
__all__ = ['module_c', 'x']
以上代码块中from . import Sub_Package导入子包,使得可以识别Package.Sub_Package;为了成功导入Sub_Package下的模块,同理需要在Sub_Package/__init__.py中显式导入相应子模块。
# Package/Sub_Package/__init__.py
from . import module_a
from . import module_b
于是,对子包中的模块的测试结果如下:
# test.py
import Package
print(Package.Sub_Package.module_b.x) # 2
子包中的导入问题¶
子包中同级模块的导入,例如Sub_Package下module_b导入module_a,可能有以下三种方式:
import module_a
# Package/Sub_Package/module_b.py
import module_a
x = module_a.x + 1
if __name__ == '__main__':
print(x) # 2
# test.py
import Package
print(Package.Sub_Package.module_b.x) # No module named 'module_a'
子模块调用时入口module_b可以正确找到同级的module_a,而test.py中入口在Package,故无法正确找到module_a。
from . import module_a
这是相对导入方式,可以解决上述问题。然而,子模块内运行却出错。这是因为**相对导入方式仅能应用于包**,而Python解释器识别包的的两个条件:
- 文件夹中必须有__init__.py文件
- 不能作为顶层模块来执行该文件夹中的py文件(即不能作为主函数的入口)
显然,直接运行子模块module_b.py违背了条件二。
# Package/Sub_Package/module_b.py
from . import module_a
x = module_a.x + 1
if __name__ == '__main__':
print(x) # cannot import name 'module_a'
# test.py
import Package
print(Package.Sub_Package.module_b.x) # 2
from Package.Sub_Package import module_a
这是绝对导入方式。当入口是Package包时,自然可以正确识别Package.Sub_Package。而直接运行子模块时,显然无法识别了。
# Package/Sub_Package/module_b.py
from Package.Sub_Package import module_a
x = module_a.x + 1
if __name__ == '__main__':
print(x) # No module named 'Package'
# test.py
import Package
print(Package.Sub_Package.module_b.x) # 2
当然也有方法使子模块正常运行——将Package所在目录加入系统搜索目录即可。但是,这样的处理却不是必要的,因为module_b最终目的是为Package所调用,并不需要直接运行它。因此以上的绝对或者相对调用方式足以满足需求;并且,相对调用方式的通用性更好,因为它不受Package的名称所影响。
# Package/Sub_Package/module_b.py
import os
import sys
script_path = os.path.abspath(__file__) # 当前module绝对路径
package_path = os.path.dirname(os.path.dirname(os.path.dirname(script_path))) # 上三级路径,即Package的父目录
sys.path.append(package_path) # 加入默认搜索路径列表
from Package.Sub_Package import module_a
x = module_a.x + 1
if __name__ == '__main__':
print(x) # 2
总结¶
__init__.py表明所在文件夹为Package,并且在import该Package时会被执行__init__.py中定义__all__指明模糊导入的模块或变量- 针对
import package的导入方式,需要在__init__.py文件中导入子模块/子包 - 注意使用相对/绝对导入方式导入子模块