一文理解Python导入机制

一文理解Python导入机制

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

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

12345678910package/ __init__.py subpackage1/ __init__.py moduleX.py moduleY.py subpackage2/ __init__.py moduleZ.py moduleA.py

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

绝对导入

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

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

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

123456789import syspath = '/path/to/package'if path not in sys.path: sys.path.insert(0, path)import moduleAimport subpackage1.moduleXfrom subpackage1 import moduleYimport subpackage2

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

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

12cd /path/to/packagepython moduleA.py # or python -m moduleA

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

123import subpackage1.moduleXfrom subpackage1 import moduleYimport subpackage2

相对导入

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

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

12from . import moduleAfrom .subpackage1 import moduleX

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

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

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

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

1from __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。

💫 相关推荐

淘宝4星要多少信誉?如何提升信誉等级?
365限制投注额度怎么办

淘宝4星要多少信誉?如何提升信誉等级?

📅 08-12 👁️ 4308
led冲电灯一般充多久 LED充电式小手电充电时间?
365bet.com官网

led冲电灯一般充多久 LED充电式小手电充电时间?

📅 08-23 👁️ 7495
李商隐《柳枝五首有序》原文,注释,译文,赏析
365限制投注额度怎么办

李商隐《柳枝五首有序》原文,注释,译文,赏析

📅 07-31 👁️ 9131