在编写自定义 Python 包时,经常需要在包内,进行文件(模块)间的相互引用。
而编写的各个功能文件,往往是需要进行单元测试的,有时为了简单,仅仅使用 if __name__ == "__main__":
的方式进行测试。但若文件引用编写不当,就容易出现 import 相关异常。
本文详细解释了 Python 加载包/模块时的种种情况,耐心读完必有收获(重点:第 0 节和第 5 节)。
Python 包/模块加载(import
)时,会按一定的路径规则进行搜索,这些被搜索的路径包括:
- The directory containing the input script (or the current directory when no file is specified).
- PYTHONPATH (a list of directory names, with the same syntax as the shell variable PATH).
- The installation-dependent default
而这些路径最终被放在了 sys.path
这个列表中。
sys.path
生成时,首先会按 PYTHONPATH
进行初始化,之后再将运行的脚本路径插入到列表首部。也可以在代码中,使用 sys.path.append
或 sys.path.insert
添加自己指定的路径。
Python 包/模块加载时,会按照 sys.path
中指定的先后顺序去搜索包/模块,一旦搜索到就返回,不再搜索其他路径。
import sys
import json
from mypkg.aaa import *
# 格式化输出 sys.path
print(json.dumps(sys.path, indent=4))
test()
setting.py
:
something = 1
aaa.py:
import sys
import json
from .setting import something
def test():
print(something)
if __name__ == "__main__":
# 格式化输出 sys.path
print(json.dumps(sys.path, indent=4))
test()
__init__.py
为空。
注意:只有在 Python 包内的文件互相导入时,才可以使用相对路径导入。当然使用绝对路径导入也是可以的(from mypkg.setting import *
),但包内使用相对路径可以防止无意间导入标准模块。
注意:使用命令行运行 py 文件,与用 PyCharm 运行 Py 文件是有所不同的,因为 PyCharm 默认将工程根目录作为了 Content Root,这会导致两者的 sys.path
有所区别(下文会具体解释):
test.py
命令行运行,正常:
Pycharm 运行,正常:
我们可以看到,使用 PyCharm 运行时,Content Root 被添加到了 sys.path
(路径重复了两次)中,但是此时还不能确定 Contetnt Root 和 脚本所在目录 被插入到列表中的顺序。
aaa.py
命令行运行,异常:
PyCharm 运行,异常:
这是由于,在单独运行文件 aaa.py
时,这个文件作为了主模块,而主模块的名字永远是 __main__
,并且相对引用是依据原模块名称来的,最终导致了相对引用失败,运行异常。
P.S.:我们单独运行
test.py
时,主模块并不是我们自定义包内的文件(而是test.py
),相对引用依据的名字不会受到__main__
影响,所以引用能够有效。
Note that relative imports are based on the name of the current module. Since the name of the main module is always “
__main__
”, modules intended for use as the main module of a Python application must always use absolute imports.
aaa.py
的 import 为绝对引用修改文件中 from .setting import something
为 from mypkg.setting import something
。
命令行运行 aaa.py
,异常:
PyCharm 运行 aaa.py
,正常:
注释 import 语句,使用命令行运行,观察结果:
对比结果我们会发现,sys.path
的初始化顺序如下:
PYTHONPATH
中的内容放入sys.path
首部sys.path
首部所以,from mypkg.setting import something
语句在:
pypkgtest/mypkg
及其他目录下定位到 mypkg
模块/包 ,运行失败pypkgtest/
)目录下定位到了 mypkg
模块/包 ,运行成功PS:修改
aaa.py
为绝对路径import
,并不影响test.py
运行成功(命令行、PyCharm均可),此处截图略。
我们将 mypkg/aaa.py
重命名为 mypkg/mypkg.py
。
命令行 或 Pycharm 运行 mypkg.py
,运行均异常:
结合我们在第 3 节得到的 sys.path
分析:
# exe by shell
[
"D:\\workspace\\pypkgtest\\mypkg",
"C:\\Users\\sigmarising\\AppData\\Local\\Programs\\Python\\Python36\\python36.zip",
"C:\\Users\\sigmarising\\AppData\\Local\\Programs\\Python\\Python36\\DLLs",
"C:\\Users\\sigmarising\\AppData\\Local\\Programs\\Python\\Python36\\lib",
"C:\\Users\\sigmarising\\AppData\\Local\\Programs\\Python\\Python36",
"C:\\Users\\sigmarising\\AppData\\Roaming\\Python\\Python36\\site-packages",
"C:\\Users\\sigmarising\\AppData\\Local\\Programs\\Python\\Python36\\lib\\site-packages"
]
# exe by PyCharm
[
"D:\\workspace\\pypkgtest\\mypkg",
"D:\\workspace\\pypkgtest",
"C:\\Users\\sigmarising\\AppData\\Local\\Programs\\Python\\Python36\\python36.zip",
"C:\\Users\\sigmarising\\AppData\\Local\\Programs\\Python\\Python36\\DLLs",
"C:\\Users\\sigmarising\\AppData\\Local\\Programs\\Python\\Python36\\lib",
"C:\\Users\\sigmarising\\AppData\\Local\\Programs\\Python\\Python36",
"C:\\Users\\sigmarising\\AppData\\Roaming\\Python\\Python36\\site-packages",
"C:\\Users\\sigmarising\\AppData\\Local\\Programs\\Python\\Python36\\lib\\site-packages",
"C:\\Program Files\\JetBrains\\PyCharm 2018.3.3\\helpers\\pycharm_matplotlib_backend"
]
我们可以得知,from mypkg.setting import something
最先搜索的路径,是 D:\\workspace\\pypkgtest\\mypkg
。
很遗憾,此路径下,我们的脚本文件 pypkgtest/mypkg/mypkg.py
在 from mypkg.setting
解析时,被作为文件(模块)搜索到了,导致自身 import 了自身!直接运行失败了。
P.S.:不过,这并不影响
test.py
在命令行 或 PyCharm 的正常运行。由第 2.1 节可得知,在最先搜索的路径D:\\workspace\\pypkgtest
中,from mypkg.setting
解析时可以成功定位到pypkgtest/mypkg/setting.py
,所以自然不会出错了。
Python 加载包/模块时,会在 sys.path
中按序搜索。
而 sys.path
的初始化顺序如下:
PYTHONPATH
中的内容放入sys.path
首部sys.path
首部当搞不明白 import 的相关异常时,不妨打印出 sys.path
检查一下。
参考资料:Python3.6 Docs - Modules