本文主要讨论 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》