一、出错场景
如果当前的工作路径就是在package1中(在此目录下执行a.py),而且在a.py中尝试用相对导入的方式引入b文件,就会导致出错。
|package1
-__init__.py
-a.py
-b.py
#a.py
from .b import *
二、出错原因
正如错误提示所将的一样,这是由于“在不知道父包的情况下尝试相对导入”导致出错。
我们知道,python中是允许进行相对导入的,即通过.或者..引入其相应的模块。但是这种相对导入机制的原则是:
相对导入是使用模块的__name__属性来确定该模块在包层次结构中的位置
上面的模块是指要导入其他模块的模块,如上面的a.py模块。
当我们在package1所在路径下直接执行a.py时,我们可将a的__name__属性进行输出,结果为"main"(如下所示)。
#a.py
print(__name___)
from .b import *
输出:
"__main__"
"ImportError: attempted relative import with no known parent package"
由于此时模块a的__name__属性不包含任何包信息(它被设置为’main’),此时解析器认为模块a是顶级模块,其无法得知模块a的父包的信息,我们使用.或..就会导致出错。
那正常情况下使用相对导入时模块a的__name__属性是怎样的呢?比如当模块a不再是直接的执行文件时,当我们把调用模块a中函数的语句写在package1外面的test.py时,依旧在文件a中打印__name__变量。
|package1
-__init__.py
-a.py
-b.py
test.py
#test.py
from package1.a import main
main()
#a.py
print(__name___)
from .b import *
输出:
"package1.a"
此时我们可以看到正常情况下的父包信息(即“package1.a"), 由于有了父包的信息,解释器将"from .b import *“解析为”from package1.b import *”, 从而能够正常地在模块a中使用相对导入的方式导入模块b。
三、解决方案
1. 不直接将模块a作为执行文件
在上面的解释过程中我们已经得到了一种解决方案,即我们将在模块a中要执行的代码放到包外的文件test.py中, 从而使得不直接执行a.py而是执行test.py, 可以使模块a中的相对导入不报错。
|package1
-__init__.py
-a.py
-b.py
test.py
事实上,这也是在成熟的项目开发中使用的方式,即不会在包内直接执行文件。但有些时候我们只是跑一些demo,希望能够即时测试,那么可以使用以下方式。
2. 使用绝对导入
将a.py的导入修改为以下格式,可避免出错。(该方法只适用于单层相对引入,如果时多层相对引入,还要使用下面的方法3)
#a.py
from b import *
此时模块a的__name__属性仍然是"main",但由于我们不使用相对导入,就不需要利用__name__属性查找父包信息并根据相对路径找到模块b,而是直接根据模块的搜索路径寻找模块b。
在这里简单介绍下模块的搜索路径有哪些及其优先级:
当我们导入一个模块b时,解释器首先搜索具有该名称的内置模块(Python语言的内置模块)。如果没有找到,它就会在变量sys.path给出的目录列表中搜索一个名为b.py的文件。sys.path(即系统路径)是由以下这些路径组成的:
1.包含输入脚本的目录(或者没有指定文件时的当前目录)。
2.PYTHONPATH(目录名列表,语法与shell变量PATH相同)。
3.用户安装的包所在的路径。
sys.path举例:
- 比如我们上述脚本文件a所在的目录package1/
- PYTHONPATH由用户执行文件时指定,比如我们运行a文件时,可以使用以下的命令指定PYTHONPATH,从而添加导入模块的搜索路径。
PYTHONPATH=…/ python a.py - 即我们安装的numpy、sklearn这些包的路径,由python自动维护
所以当解释器要寻找模块b时,它根据sys.path中的”package1/"可以顺利找到该模块,从而不会报错。
3. 添加sys.path或修改PYTHONPATH
(1)添加sys.path
根据2中的介绍我们知道模块导入时寻找路径有哪些。假设我们想要绝对引入的模块不在文件a所在的目录时,我们可采用添加sys.path的方式解决该错误。
|package1
-__init__.py
-a.py
-b.py
|package2
-__init__.py
-c.py
比如当我们想要在a.py中使用绝对导入方式导入模块c时,我们无法通过a.py所在目录路径"package1/"找到模块c,这时我们可以通过sys.path.append()的方式添加模块c所在的目录路径,从而使得解释器能找到模块c,如下所示。
#a.py
import sys
sys.path.append('../')
from package2.c import *
"""或者:
sys.path.append('../package2/')
from c import *
"""
值得注意的是,我们添加的系统路径只在程序运行时有效,程序停止后恢复默认搜索路径。
(2)修改PYTHON PATH
另外我们也可以在执行bash命令时添加PYTHONPATH来增加系统路径, 可有效避免该错误,比如:
amind@desktiop:~/package1$ PYTHONPATH '../' python a.py
此时a.py中的内容应该为:
#a.py
from package2.c import *
四、参考资料
【1】https://www.python.org/dev/peps/pep-0328/
【2】https://docs.python.org/3/tutorial/modules.html?highlight=import#the-module-search-path
【3】https://blog.csdn.net/xiongchengluo1129/article/details/80453599