本文主要讨论 Python 3 的 import。
导入步骤
import 语句主要执行以下两个步骤:
搜索模块;
搜索结果绑定到局部命名空间。
本文主要关注搜索步骤的逻辑。
搜索模块
搜索模块分为两个过程:
搜索
sys.modules;搜索
sys.meta_path。
sys.modules
首次导入一个模块时相关程序会将此模块以及模块中调用到的其他模块信息以字典的形式保存到sys.modules 中,如果再次import此模块则会直接使用字典中的信息。
如果是通过 from package import module 或 import package.module 形式导入的模块则其父包(模块)也会被写入 sys.modules 中。
我们可以直接修改 sys.modules 来改变缓存信息:
1 | import os |
sys.meta_path
如果在 sys.modules 中没有找到模块,则进入第二个搜索步骤:使用 sys.meta_path 搜索。
sys.meta_path 是保存了一系列 importer 对象的list。根据官方定义,importer 是指实现了 finders 和 loaders 接口的对象。此处我们暂时不纠结于此定义,直接看 sys.meta_path 中有什么。
Python 3.6的 sys.meta_path中默认有以下三个 importer:
- class ‘_frozen_importlib.BuiltinImporter’
- class ‘_frozen_importlib.FrozenImporter’
- class ‘_frozen_importlib.PathFinder’
三个 importer 的用途分别是:查找及导入build-in模块;查找及导入frozen模块(即已编译为Unix可执行文件的模块);查找及导入import path中的模块。
如果这三个 importer 中都没找到模块则会抛出 ModuleNotFoundError。
import hooks
如果要扩展 import 的行为,需要用到import hooks。
Python 中有两种import hooks:meta hooks 和 path hooks。
meta hooks
meta hooks 简单来说是通过修改 sys.meta_path 达到拦截导入目的。
上面提到过,Importer 对象需要实现 finders 协议和 loaders 协议。其中finders协议需要提供 find_spec() 方法(Python3.4以后的推荐方法)或 find_modules()方法。此处我们自己定义两个 importer (暂不考虑loaders协议)来演示如何进行meta hook:
1 | class Importer1(object): |
把两个 importer 分别插入 sys.meta_path 的首尾:
1 | import sys |
测试导入:
1 | >>import json |
首先我们导入了标准库中的json, 发现只调用了Importer1中的find_spec方法,并且最后导入成功。之后导入一个不存在的模块,Importer1 中的 find_spec 和 Importer2 中的 find_spec 方法均被调用到,且模块没有导入成功。
通过上面的示例可以看出使用meta hooks的方式:定义符合协议的 importer 并插入 sys.meta_path 中。 sys.meta_path 是有序列表,如果想尽早拦截 import 则需将自定义的 importer 插入 sys.meta_path首位。
Python源码中的BuiltinImporter写法如下:
1 | class BuiltinImporter: |
finders 与 loaders
上面的例子已经演示了 finders 协议。finders协议主要任务是:根据方法定义查找模块,如果找不到则返回None,如果找到则返回一个spec类型实例(Py3.4之前是返回 loaders)。 spec实例封装了一些导入模块所需信息,以及loader方法,loader方法需实现loaders协议功能。
loaders 主要任务是:在找到模块后做一些初始化操作。loaders 必须要实现exec_module() 方法(在py3.4之前需要实现load_module()方法)。另外还可以实现可选的 create_module() 方法,用以创建模块。
path hooks
另一种import hooks 叫path hooks, 作用于sys.meta_path 中的 PathFinder。PathFinder 也是一个 Importer, 意味着它实现了 find_spec 方法。但由于不同路径的模块文件类型不同,在不同的路径下搜索需要实现不同的逻辑,并且判断路径的处理逻辑也是耗时的操作。
Python中用以下方法处理此问题:PathFinder 仍然作为入口,但具体处理逻辑(也就是针对具体情况的Importer代码)独立为其他模块。
当有新的搜索需求到达 PathFinder 时它首先开始迭代搜索路径(默认为sys.path),并将路径依次传递给sys.path_hooks中的可调用对象,如果某个对象可以处理此路径则将其写入 sys.path_importer_cache中并返回此结果用于接下来的模块搜索。之后如果再次遇到此路径则会直接用 path_importer_cache 中缓存的信息处理。
下面是源码中 PathFinder 的部分代码:
1 |
|
我们直接定义一个用于判断的函数并将其加入path_hooks即可。下面是《Python cookbook》中用于判断路径是否是URL的示例代码:
1 | def check_url(path): |
如果此路径符合要求则返回处理此路径的 Importer ,否则抛出 ImportError 错误,PathFinder 会继续尝试path_hooks中的下一个。
返回的 Importer 对象和前面的 Importer 类似,也需要定义 find_spec()。更多细节请参考官方文档。
参考资料:
- The import system
- 《Python Cookbook, 3rd Edition》