干货:弄懂 Python 包的加载 && 解决自定义包内 py 文件单独运行时,包内文件引用错误

干货:弄懂 Python 包的加载 && 解决自定义包内 py 文件单独运行时,包内文件引用错误


在编写自定义 Python 包时,经常需要在包内,进行文件(模块)间的相互引用。

而编写的各个功能文件,往往是需要进行单元测试的,有时为了简单,仅仅使用 if __name__ == "__main__": 的方式进行测试。但若文件引用编写不当,就容易出现 import 相关异常。

本文详细解释了 Python 加载包/模块时的种种情况,耐心读完必有收获(重点:第 0 节和第 5 节)。


0. Python 的包加载

Python 包/模块加载(import)时,会按一定的路径规则进行搜索,这些被搜索的路径包括

  1. The directory containing the input script (or the current directory when no file is specified).
  2. PYTHONPATH (a list of directory names, with the same syntax as the shell variable PATH).
  3. The installation-dependent default

而这些路径最终被放在了 sys.path 这个列表中

sys.path 生成时,首先会按 PYTHONPATH 进行初始化,之后再将运行的脚本路径插入到列表首部。也可以在代码中,使用 sys.path.appendsys.path.insert 添加自己指定的路径。

Python 包/模块加载时,会按照 sys.path 中指定的先后顺序去搜索包/模块,一旦搜索到就返回,不再搜索其他路径。


1. 新建工程

使用 PyCharm 编写工程如下:
干货:弄懂 Python 包的加载 && 解决自定义包内 py 文件单独运行时,包内文件引用错误_第1张图片
test.py:

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 *),但包内使用相对路径可以防止无意间导入标准模块。


2. 分别运行

注意:使用命令行运行 py 文件,与用 PyCharm 运行 Py 文件是有所不同的,因为 PyCharm 默认将工程根目录作为了 Content Root,这会导致两者的 sys.path 有所区别(下文会具体解释):
干货:弄懂 Python 包的加载 && 解决自定义包内 py 文件单独运行时,包内文件引用错误_第2张图片

2.1 运行 test.py

命令行运行,正常:
干货:弄懂 Python 包的加载 && 解决自定义包内 py 文件单独运行时,包内文件引用错误_第3张图片
Pycharm 运行,正常:
干货:弄懂 Python 包的加载 && 解决自定义包内 py 文件单独运行时,包内文件引用错误_第4张图片
我们可以看到,使用 PyCharm 运行时,Content Root 被添加到了 sys.path (路径重复了两次)中,但是此时还不能确定 Contetnt Root 和 脚本所在目录 被插入到列表中的顺序。

2.2 运行 aaa.py

命令行运行,异常:
干货:弄懂 Python 包的加载 && 解决自定义包内 py 文件单独运行时,包内文件引用错误_第5张图片
PyCharm 运行,异常:
干货:弄懂 Python 包的加载 && 解决自定义包内 py 文件单独运行时,包内文件引用错误_第6张图片
这是由于,在单独运行文件 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.


3. 修改 aaa.py 的 import 为绝对引用

修改文件中 from .setting import somethingfrom mypkg.setting import something

命令行运行 aaa.py,异常:
干货:弄懂 Python 包的加载 && 解决自定义包内 py 文件单独运行时,包内文件引用错误_第7张图片
PyCharm 运行 aaa.py,正常:
干货:弄懂 Python 包的加载 && 解决自定义包内 py 文件单独运行时,包内文件引用错误_第8张图片
注释 import 语句,使用命令行运行,观察结果:
干货:弄懂 Python 包的加载 && 解决自定义包内 py 文件单独运行时,包内文件引用错误_第9张图片

对比结果我们会发现,sys.path 的初始化顺序如下:

  1. PYTHONPATH 中的内容放入
  2. 如果使用 PyCharm 运行,PyCharm 会把工程设置的 Content Root 插入到 sys.path 首部
  3. 将运行脚本所在的目录,插入到 sys.path 首部

所以,from mypkg.setting import something 语句在:

  • 命令行运行时:无法在 pypkgtest/mypkg 及其他目录下定位到 mypkg 模块/包 ,运行失败
  • PyCharm运行时:成功在 Content Root(pypkgtest/)目录下定位到了 mypkg 模块/包 ,运行成功

PS:修改 aaa.py 为绝对路径 import,并不影响 test.py 运行成功(命令行、PyCharm均可),此处截图略。


4. 其他注意事项:包内文件名与自定义包同名

我们将 mypkg/aaa.py 重命名为 mypkg/mypkg.py

命令行 或 Pycharm 运行 mypkg.py,运行均异常:
干货:弄懂 Python 包的加载 && 解决自定义包内 py 文件单独运行时,包内文件引用错误_第10张图片
干货:弄懂 Python 包的加载 && 解决自定义包内 py 文件单独运行时,包内文件引用错误_第11张图片
结合我们在第 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.pyfrom mypkg.setting 解析时,被作为文件(模块)搜索到了,导致自身 import 了自身!直接运行失败了。

P.S.:不过,这并不影响 test.py 在命令行 或 PyCharm 的正常运行。由第 2.1 节可得知,在最先搜索的路径 D:\\workspace\\pypkgtest 中, from mypkg.setting 解析时可以成功定位到 pypkgtest/mypkg/setting.py,所以自然不会出错了。


5. 总结

Python 加载包/模块时,会在 sys.path 中按序搜索。

sys.path 的初始化顺序如下:

  1. PYTHONPATH 中的内容放入
  2. 如果使用 PyCharm 运行,PyCharm 会把工程设置的 Content Root 插入到 sys.path 首部
  3. 将运行脚本所在的目录,插入到 sys.path 首部

当搞不明白 import 的相关异常时,不妨打印出 sys.path 检查一下。

参考资料:Python3.6 Docs - Modules

你可能感兴趣的:(Python,Python,import,模块/包引用)