Python 的 import 是怎么工作的?
你好,我是 somenzz,可以叫我征哥。
Python 的 import 是非常直观的,但即使这样,有时候你会发现,明明包就在那里,我们仍会遇到 ModuleNotFoundError
,明明相对路径非常正确,就是报错
ImportError: attempted relative import with no known parent package
导入同一个目录的模块和不同的目录的模块是完全不同的,本文通过分析使用 import 经常遇到的一些问题,来帮助你轻松搞定 import ,据此,你可以轻松创建属于自己的包。
主要内容:
0、什么是模块(module),什么是包(package)
1、import 时发生了什么
2、什么时候用相对导入,什么时候用绝对导入
3、一个自定义包的例子
0、什么是模块(module),什么是包(package)
模块(module)
模块与包的关系,可以类比文件和目录,模块就是文件。
Python 文档中这样描述,一个 Python 文件就是一个模块,Python 的文件名(不带后缀.py)就是模块名。
一个 module 可以包含变量、函数和类,它们是该 module 定义的命名空间的一部分,因此变量的命名问题不是问题,因为两个不同的模块可以有同名的变量、函数和类。
包(package)
模块与包的关系,可以类比文件和目录,包就是目录。
package 里面可以有 module,也可以有子包(sub-package)。一个模块定义一个命名空间,以便变量、函数和类可以在两个不同的模块中具有相同的名称,同样的,一个包对其组成的包和模块做同样的事情,可以通过点号访问主包中的模块和包。
一个基本的 package 可以包含 sub-package、modules、__init__.py
(Python 3.3 之后非必需)、setup.py。一个可能的 package 结构如下所示:
而 setup.py 存在于你的 package 所在的主目录中,包含配置信息,如所需的依赖项、脚本和子包。你还可以指定有关 package 的元数据,例如 package 的名称、作者、描述等。
setup.py 是 pip 用来安装你的包的文件。
1、import 时发生了什么
先举一个简单的例子,比如说同一个目录有两个文件,file1.py 和 file2.py,内容很简单,就打印各自的文件名,不同的是 file2.py 里面 import 了 file1:
#file1.py
print("This is file1.py")
#file2.py
print("This is file2.py")
import file1
运行 file2.py 可以得到下面的结果:
❯ python file2.py
This is file2.py
This is file1.py
可以看出:
import 很直观,用谁就 import 谁。 import 语句就是一个普通的语句,可以放在任何位置。 一个文件被 import 的时候,就会被执行,其内部的类或对象将添加到其命名空间。
我们还要知道 import 的搜索顺序,只需要记住一点,那就是 import 会去 sys.path 里面搜索。
比如我在 file2.py 的末尾添加一行代码:import sys; print(sys.path)
就可以打印 import 的搜索路径:
可以看出 sys.path 的顺序
会先搜索执行脚本所在的路径 标准库 第三方库 site-packages
关于 sys.path 需要你注意的是
1、在解释器环境下,sys.path[0] 就是解释器启动时所在的路径 ''
2、sys.path 并不会依赖当前程序的工作路径 - os.getcwd(),仅仅依赖第一个脚本所在的路径:
3、如果一个模块导入另一个模块,而后者又导入另一个模块,则第一个模块的 sys.path 是解释器搜索第二个导入语句的位置。
一旦模块或包被找到,就会执行该模块或包。如果包里面有初始化文件 __init__.py
,导入的时候,会先执行 __init__.py
。
然后要导入的项目就添加到了其命名空间内,我们可以通过 xx.yy 的方式来使用。
2、什么时候用相对导入,什么时候用绝对导入
先看看什么是绝对导入,所谓绝对导入就是这样的形式:
import aa
import aa.bb
from aa import bb
这样的方式很直观, import 会去 sys.path 查找就行了,如果遇到了 ModuleNotFoundError,思考一下为什么 sys.path 没有我们要导入的包,或者手动把这个包的路径插入到 sys.path 中去。
再看看什么是相对导入,所谓相对导入就是这样的形式:
from . import aa
from .aa import bb
from .. import yy
也就是说相对路径中有个 . 号,用来表明要导入的模块或当前的包的相对位置。
举个例子,我们 pythonimportexample 目录下新建一个目录 subpackage1,在 subpackage1 内新建两个文件 file3.py、file4.py,
内容如下:
file3.py :
print("This is file3.py")
file4.py
from . import file3
print("This is file4.py")
只要我们直接运行 file4.py,那是一定会报错的:
Python 提示我们:
ImportError: attempted relative import with no known parent package
也就是说相对导入不知道父包是谁,换句话说,这是一个子包,必须让父包来调用它,直接运行这个文件是不行的,即使你在 file4.py 的目录 subpackage1 同级的目录执行该文件也是不行的,见上图。
但是在 file4.py 的目录 subpackage1 同级的目录作为一个 module 来执行是可以的,如下图:
换句话说,我们把 subpackage1 作为一个包来让别人用,相对导入是可以的,比如说我们在目录 subpackage1 同级的目录新建一个 file5.py 的文件,内容如下:
file5.py:
from subpackage1 import file4
然后,执行 python file5.py 可以看出,相对导入已经正常工作:
结论
如果是当做脚本文件直接运行的,使用绝对导入 如果是当做模块供其他文件导入,使用相对导入
3、一个自定义包的例子
先上一个图来看下目录及引用结构,方块的是目录,椭圆的是文件,曲线是引用:
其中 import_example 目录下有 setup.py 和 run.py
run.py 导入了 file4、file5、file6。
file4 导入了 file3,file5 导入了 file3
file6 导入了 file2,file2 导入了 file1
现在我们来执行一下 run.py 看下效果:
可以看出所有相对导入都已正常工作,虽然 file3 被导入了两次,但只执行了一次,说明 Python 内部已经考虑了同一个包的多重导入问题。
自定义包就是让其他文件导入使用的,因此 pythonimportexample目录下都使用相对导入,源代码见:
https://gitee.com/somenzz/code-example/tree/master/import_example
这里还有一些自定义包的例子:
dbinterface[1]
transferfile[2]
最后的话
本文分享了什么是模块(module),什么是包(package),import 的搜索路径,也分享了相对导入和绝对导入的区别,最后举了一个非常实用的 import 例子,方便你构建自己的包。
参考资料
dbinterface: https://github.com/somenzz/dbinterface
[2]transferfile: https://github.com/somenzz/transferfile