一文理解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
。