一文理解Python导入机制
Python 的 import 机制是最令用户困惑的地方之一,在实践中非常容易出错,相信被 ImportError 和 ModuleNotFoundError 折磨过的同学都对此深有体会。本文完整地梳理 Python 的各种导入逻辑,力求在实践中避坑并提出一些最佳实践。
PS:本文中的导入语句均以如下所示的目录结构为例进行演示:
| 1 | package/ | 
Python 的导入行为可以分为绝对导入与相对导入两类:
绝对导入
绝对导入即指定 package 或 module 的绝对名称或路径,经常用于导入内置库或第三方库,例如:
| 1 | import os # 导入内置库 | 
事实上,绝对导入是通过依次搜索 sys.path 列表中的所有路径来完成的,这一点类似于操作系统的 PATH 环境变量。一个目录只要加入到了 sys.path 中,那么其中直接包含的任意 package 或 module 均可实行绝对导入。例如:
| 1 | import sys | 
除了在代码运行时动态添加 sys.path 外,还有一个环境变量可以在 Python 进程启动时设定 sys.path 的初始值,即 PYTHONPATH。一些需要经常引用的本地目录可以加入 PYTHONPATH 中,这样就不用每次都在代码中修改 sys.path 了。
此外,通过 Python 命令启动脚本或模块时会把父进程(通常是命令行)的当前目录加入 sys.path 中,因此当前目录下的任意 package 或 module 也可以直接进行绝对导入。例如在 shell 中执行如下命令调用 Python 脚本:
| 1 | cd /path/to/package | 
那么在 moduleA.py 中可以进行以下绝对导入:
| 1 | import subpackage1.moduleX | 
相对导入
相对导入是指同一个顶层 package 内部不同 module 之间的导入行为,这是大前提。很多文章包括官方文档在讲解相对导入时往往没有强调这个前提,导致大量的误解。
相对导入包含一个或多个前导的 .,其格式为 from .xxx import yyy,例如:
| 1 | from . import moduleA | 
其中,. 代表当前 package,.. 代表上层 package,... 代表上上层 package,以此类推。
此外,还有一种称为“隐式相对导入”的方式,其导入语句格式与绝对导入完全一样。例如在 moduleX 中引用 moduleY:
| 1 | import moduleY # 隐式相对导入 | 
隐式相对导入容易与绝对导入混淆,非常不推荐,已被 Python3 废弃。如果希望在使用 Python2 时也废弃这种语法,可以在代码中加上以下语句:
| 1 | from __future__ import absolute_import | 
后文中所提到的相对导入如无特殊说明均指显式相对导入。
在相对导入中,当目录结构与导入语句均确定时,能否断定一个绝对导入/相对导入一定正确或错误的?
答案是不能,需要视情况而定。根据程序调起的方式不同,同样的 import 语句有时候是正确的,有时候会报错。这就是相对导入最让人困惑的地方。
对此,我们需要了解 Python 程序的不同调起方式。
Python 程序的三种调起方式
- 作为脚本直接运行:python package/subpackage1/moduleX.py
- 作为模块直接运行:python -m package.subpackage1.moduleX
- 从别的模块中导入:import package.subpackage1.moduleX
Python 的导入机制依赖 sys.path、__package__ 和 __name__ 三个变量。以上三种调用方式会对这三个变量产生不同的作用。当执行到 moduleX.py 内部时,有:
- 方式 1:__package__为None;__name__为'__main__'; 当前目录和脚本所在目录被加入sys.path。
- 方式 2:__package__为'package';__name__为'__main__'; 当前目录被加入sys.path。
- 方式 3:__package__为'package';__name__为'moduleA'。sys.path中具体加入了什么路径,要看程序入口是怎么调起的。
所谓相对导入,相对的就是 __package__ 所代表的包名。当执行 from .moduleY import func 时,实际上相当于解析 from __package__.moduleY import func。如果 __package__ 为 None,则会解析 from __name__.moduleY import func(后文讨论 __package__ 的取值时,均已包含该降级逻辑)。
相对导入可能抛出的错误包括以下几种:
- 
如果 __package__为''(一般出现在类似python -m moduleX这样的调用方式中),会抛出ImportError: attempted relative import with no known parent package错误。
- 
如果 __package__不为空,但在sys.path的所有路径中均未搜索到__package__所代表的包名,会抛出类似ModuleNotFoundError: No module named 'xxxpackage.moduleY'; 'xxxpackage' is not a package的错误。
- 
如果 __package__不为空且存在对应的包,但其中没有moduleY模块,会抛出类似ModuleNotFoundError: No module named 'xxxpackage.moduleY'的错误。
- 
如果试图从上级 package 中进行相对导入,例如 from ..moduleA import func,那么必须确保__package__是多级 package,例如__package__ = 'package.subpackage1'。如果 package 级别数小于上溯的级别数,例如__package__ = 'subpackage1',将会抛出ValueError: attempted relative import beyond top-level package错误。
由此可见,相对导入必须确保 __package__ 有合适的取值,也就是只能用于上述第 2、3 种调起方式。尽管第 1 种调起方式是最常用的,但不幸的是在这种方式下只能使用绝对导入,不能使用相对导入。
绝对导入与相对导入对比
绝对导入由于其含义非常明确,且在任何调起方式中均可以使用,因而被 PEP8 所推荐。
绝对导入唯一的缺点是将 package 名称硬编码到了代码中,会带来维护问题。例如修改了某一顶层包名之后,那么其内部的所有绝对导入代码都需要相应修改。
而相对导入就可以避免这种维护问题,当包名修改之后内部代码无需做任何改动。但相对导入的解析机制更加复杂,容易因为使用不当而报错。并且使用了相对导入的 py 文件无法再作为脚本直接运行。
最佳实践
结合绝对导入与相对导入二者的优缺点,推荐一种关于绝对导入与相对导入的最佳实践:
- 一般情况下使用绝对导入。
- 如果要构建一个 package 供外部调用,例如给其他脚本调用或发布到 PYPI,则在该 package 内部使用相对导入。
- 对于使用了相对导入的脚本,如果想直接运行其中的 if __name__ == '__main__':代码块(通常用于简单测试当前 module 的功能),可以使用python -m package.module的方式调起,避免使用python package/module.py。