今天的Python学习教程(学习路线)跟大家讨论一下关于模块导入,一些精华的地方!
所谓的模块导入,是指在一个模块中使用另一个模块的代码的操作,它有利于代码的复用。
也许你看到这个标题,会说我怎么会发这么基础的文章?(当然也会有基础的文章啦)
与此相反。恰恰我觉得这篇文章的内容可以算是 Python 的进阶技能,会深入地探讨并以真实案例讲解 Python import Hook 的知识点。
当然为了使本次的Python学习教程(学习路线)更系统、全面,前面会有小篇幅讲解基础知识点,但希望你能有耐心的往后读下去,因为后面才是本篇文章的精华所在,希望你不要错过。
1. 导入系统的基础
1.1 导入单元构成
导入单元有多种,可以是模块、包及变量等。
对于这些基础的概念,对于新手还是有必要介绍一下它们的区别。
模块:类似 .py,.pyc, .pyd ,.so,*.dll 这样的文件,是 Python 代码载体的最小单元。
包 还可以细分为两种:
- Regular packages:是一个带有 init.py 文件的文件夹,此文件夹下可包含其他子包,或者模块
- Namespace packages
关于 Namespace packages,有的人会比较陌生,我这里摘抄官方文档的一段说明来解释一下。
Namespace packages 是由多个 部分 构成的,每个部分为父包增加一个子包。各个部分可能处于文件系统的不同位置。部分也可能处于 zip 文件中、网络上,或者 Python 在导入期间可以搜索的其他地方。命名空间包并不一定会直接对应到文件系统中的对象;它们有可能是无实体表示的虚拟模块。
命名空间包的 path 属性不使用普通的列表。而是使用定制的可迭代类型,如果其父包的路径 (或者最高层级包的 sys.path) 发生改变,这种对象会在该包内的下一次导入尝试时自动执行新的对包部分的搜索。
命名空间包没有 parent/init.py 文件。实际上,在导入搜索期间可能找到多个 parent 目录,每个都由不同的部分所提供。因此 parent/one 的物理位置不一定与 parent/two 相邻。在这种情况下,Python 将为顶级的 parent 包创建一个命名空间包,无论是它本身还是它的某个子包被导入。
1.2 相对/绝对对导入
当我们 import 导入模块或包时,Python 提供两种导入方式:
- 相对导入(relative import ):import foo.bar 或者 form foo import bar
- 绝对导入(absolute import):from . import B 或 from …A import
B,其中.表示当前模块,…表示上层模块
你可以根据实际需要进行选择,但有必要说明的是,在早期的版本( Python2.6 之前),Python 默认使用的相对导入。而后来的版本中( Python2.6 之后),都以绝对导入为默认使用的导入方式。
使用绝对路径和相对路径各有利弊:
- 当你在开发维护自己的项目时,应当使用相对路径导入,这样可以避免硬编码带来的麻烦。
- 而使用绝对路径,会让你模块导入结构更加清晰,而且也避免了重名的包冲突而导入错误。
1.3 导入的标准写法
在 PEP8 中有一条,对模块的导入顺序提出了要求,不同来源模块导入,应该有清晰的界限,使用一空行来分开。
- import 语句应当分行书写
# bad
import os,sys
# good
import os
import sys
- import语句应当使用absolute import
# bad
from ..bar import Bar
# good
from foo.bar import test
- import语句应当放在文件头部,置于模块说明及docstring之后,全局变量之前
- import语句应该按照顺序排列,每组之间用一个空格分隔,按照内置模块,第三方模块,自己所写的模块调用顺序,同时每组内部按照字母表顺序排列
# 内置模块
import os
import sys
# 第三方模块
import flask
# 本地模块
from foo import bar
2. import 的妙用
在 Python 中使用 import 关键字来实现模块/包的导入,可以说是基础中的基础。
但这不是唯一的方法,还有 importlib.import_module() 和 import() 等。
对于 import ,普通的开发者,可能就会比较陌生。
和 import 不同的是,import 是一个函数,也正是因为这个原因,使得__import__ 的使用会更加灵活,常常用于框架中,对于插件的动态加载。
实际上,当我们调用 import 导入模块时,其内部也是调用了 import ,请看如下两种导入方法,他们是等价的。我记得在之前的Python学习教程中有跟大家提到过!
# 使用 import
import os
# 使用 __import__
os = __import__('os')
通过举一反三,下面两种方法同样也是等价的。
# 使用 import .. as ..
import pandas as pd
# 使用 __import__
pd = __import__('pandas')
上面我说 import 常常用于插件的动态,事实上也只有它能做到(相对于 import 来说)。
插件通常会位于某一特定的文件夹下,在使用过程中,可能你并不会用到全部的插件,也可能你会新增插件。
如果使用 import 关键字这种硬编码的方式,显然太不优雅了,当你要新增/修改插件的时候,都需要你修改代码。更合适的做法是,将这些插件以配置的方式,写在配置文件中,然后由代码去读取你的配置,动态导入你要使用的插件,即灵活又方便,也不容易出错。
假如我的一个项目中,有 plugin01 、plugin02、plugin03 、plugin04 四个插件,这些插件下都会实现一个核心方法 run() 。但有时候我不想使用全部的插件,只想使用 plugin02、plugin04 ,那我就在配置文件中写我要使用的两个插件。
# my.conf
custom_plugins=['plugin02', 'plugin04']
那我如何使用动态加载,并运行他们呢?
# main.py
for plugin in conf.custom_plugins:
__import__(plugin)
sys.modules[plugin].run()
3. 理解模块的缓存
在一个模块内部重复引用另一个相同模块,实际并不会导入两次,原因是在使用关键字import 导入模块时,它会先检索 sys.modules 里是否已经载入这个模块了,如果已经载入,则不会再次导入,如果不存在,才会去检索导入这个模块。
来实验一下,在 my_mod02 这个模块里,我 import 两次 my_mod01 这个模块,按逻辑每一次 import 会一次 my_mod01 里的代码(即打印 in mod01),但是验证结果是,只打印了一次。
$ cat my_mod01.py
print('in mod01')
$ cat my_mod02.py
import my_mod01
import my_mod01
$ python my_mod02.py
in mod01
该现象的解释是:因为有 sys.modules 的存在。
sys.modules 是一个字典(key:模块名,value:模块对象),它存放着在当前 namespace 所有已经导入的模块对象。
# test_module.py
import sys
print(sys.modules.get('json', 'NotFound'))
import json
print(sys.modules.get('json', 'NotFound')<