一文理解Python导入机制

Python 的 import 机制是最令用户困惑的地方之一,在实践中非常容易出错,相信被 ImportErrorModuleNotFoundError 折磨过的同学都对此深有体会。本文完整地梳理 Python 的各种导入逻辑,力求在实践中避坑并提出一些最佳实践。

PS:本文中的导入语句均以如下所示的目录结构为例进行演示:

1
2
3
4
5
6
7
8
9
10
package/
__init__.py
subpackage1/
__init__.py
moduleX.py
moduleY.py
subpackage2/
__init__.py
moduleZ.py
moduleA.py

Python 的导入行为可以分为绝对导入与相对导入两类:

绝对导入

绝对导入即指定 package 或 module 的绝对名称或路径,经常用于导入内置库或第三方库,例如:

1
2
import os        # 导入内置库
import requests # 导入第三方库

事实上,绝对导入是通过依次搜索 sys.path 列表中的所有路径来完成的,这一点类似于操作系统的 PATH 环境变量。一个目录只要加入到了 sys.path 中,那么其中直接包含的任意 package 或 module 均可实行绝对导入。例如:

1
2
3
4
5
6
7
8
9
import sys
path = '/path/to/package'
if path not in sys.path:
sys.path.insert(0, path)

import moduleA
import subpackage1.moduleX
from subpackage1 import moduleY
import subpackage2

除了在代码运行时动态添加 sys.path 外,还有一个环境变量可以在 Python 进程启动时设定 sys.path 的初始值,即 PYTHONPATH。一些需要经常引用的本地目录可以加入 PYTHONPATH 中,这样就不用每次都在代码中修改 sys.path 了。

此外,通过 Python 命令启动脚本或模块时会把父进程(通常是命令行)的当前目录加入 sys.path 中,因此当前目录下的任意 package 或 module 也可以直接进行绝对导入。例如在 shell 中执行如下命令调用 Python 脚本:

1
2
cd /path/to/package
python moduleA.py # or python -m moduleA

那么在 moduleA.py 中可以进行以下绝对导入:

1
2
3
import subpackage1.moduleX
from subpackage1 import moduleY
import subpackage2

相对导入

相对导入是指同一个顶层 package 内部不同 module 之间的导入行为,这是大前提。很多文章包括官方文档在讲解相对导入时往往没有强调这个前提,导致大量的误解。

相对导入包含一个或多个前导的 .,其格式为 from .xxx import yyy,例如:

1
2
from . import moduleA
from .subpackage1 import moduleX

其中,. 代表当前 package,.. 代表上层 package,... 代表上上层 package,以此类推。

此外,还有一种称为“隐式相对导入”的方式,其导入语句格式与绝对导入完全一样。例如在 moduleX 中引用 moduleY:

1
2
import moduleY                   # 隐式相对导入
from . import moduleY # 显式相对导入

隐式相对导入容易与绝对导入混淆,非常不推荐,已被 Python3 废弃。如果希望在使用 Python2 时也废弃这种语法,可以在代码中加上以下语句:

1
from __future__ import absolute_import

后文中所提到的相对导入如无特殊说明均指显式相对导入。

在相对导入中,当目录结构与导入语句均确定时,能否断定一个绝对导入/相对导入一定正确或错误的?

答案是不能,需要视情况而定。根据程序调起的方式不同,同样的 import 语句有时候是正确的,有时候会报错。这就是相对导入最让人困惑的地方。

对此,我们需要了解 Python 程序的不同调起方式。

Python 程序的三种调起方式

  1. 作为脚本直接运行:python package/subpackage1/moduleX.py
  2. 作为模块直接运行:python -m package.subpackage1.moduleX
  3. 从别的模块中导入:import package.subpackage1.moduleX

Python 的导入机制依赖 sys.path__package____name__ 三个变量。以上三种调用方式会对这三个变量产生不同的作用。当执行到 moduleX.py 内部时,有:

  1. 方式 1:__package__None; __name__'__main__'; 当前目录和脚本所在目录被加入 sys.path
  2. 方式 2:__package__'package'; __name__'__main__'; 当前目录被加入 sys.path
  3. 方式 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 文件无法再作为脚本直接运行。

最佳实践

结合绝对导入与相对导入二者的优缺点,推荐一种关于绝对导入与相对导入的最佳实践:

  1. 一般情况下使用绝对导入。
  2. 如果要构建一个 package 供外部调用,例如给其他脚本调用或发布到 PYPI,则在该 package 内部使用相对导入。
  3. 对于使用了相对导入的脚本,如果想直接运行其中的 if __name__ == '__main__': 代码块(通常用于简单测试当前 module 的功能),可以使用 python -m package.module 的方式调起,避免使用 python package/module.py