8.10 【进阶】理解查找器与加载器

如果指定名称的模块在 sys.modules 找不到,则将发起调用 Python 的导入协议以查找和加载该模块。

此协议由两个概念性模块构成,即 查找器加载器

一个 Python 的模块的导入,其实可以再细分为两个过程:

  1. 由查找器实现的模块查找

  2. 由加载器实现的模块加载

1. 查找器是什么?

查找器(finder),简单点说,查找器定义了一个模块查找机制,让程序知道该如何找到对应的模块。

其实 Python 内置了多个默认查找器,其存在于 sys.meta_path 中。

但这些查找器对应使用者来说,并不是那么重要,因此在 Python 3.3 之前, Python 解释将其隐藏了,我们称之为隐式查找器。

# Python 2.7
>>> import sys
>>> sys.meta_path
[]
>>>

由于这点不利于开发者深入理解 import 机制,在 Python 3.3 后,所有的模块导入机制都会通过 sys.meta_path 暴露,不会在有任何隐式导入机制。

# Python 3.6
>>> import sys
>>> from pprint import pprint
>>> pprint(sys.meta_path)
[<class '_frozen_importlib.BuiltinImporter'>,
 <class '_frozen_importlib.FrozenImporter'>,
 <class '_frozen_importlib_external.PathFinder'>]

观察一下 Python 默认的这几种查找器 (finder),可以分为三种:

  • 一种知道如何导入内置模块

  • 一种知道如何导入冻结模块

  • 一种知道如何导入来自 import path 的模块 (即 path based finder)。

那我们能不能自已定义一个查找器呢?当然可以,你只要

  • 定义一个实现了 find_module 方法的类(py2和py3均可),或者实现 find_loader 类方法(仅 py3 有效),如果找到模块需要返回一个 loader 对象或者 ModuleSpec 对象(后面会讲),没找到需要返回 None

  • 定义完后,要使用这个查找器,必须注册它,将其插入在 sys.meta_path 的首位,这样就能优先使用。

import sys

class MyFinder(object):
    @classmethod
    def find_module(cls, name, path, target=None):
        print("Importing", name, path, target)
        # 将在后面定义
        return MyLoader()

# 由于 finder 是按顺序读取的,所以必须插入在首位
sys.meta_path.insert(0, MyFinder)

查找器可以分为两种:

object
 +-- Finder (deprecated)
      +-- MetaPathFinder
      +-- PathEntryFinder

这里需要注意的是,在 3.4 版前,查找器会直接返回 加载器(Loader)对象,而在 3.4 版后,查找器则会返回模块规格说明(ModuleSpec),其中 包含加载器。

而关于什么是 加载器 和 模块规格说明, 请继续往后看。

2. 加载器是什么?

查找器只负责查找定位找模,而真正负责加载模块的,是加载器(loader)。

一般的 loader 必须定义名为 load_module() 的方法。

为什么这里说一般,因为 loader 还分多种:

object
 +-- Finder (deprecated)
 |    +-- MetaPathFinder
 |    +-- PathEntryFinder
 +-- Loader
      +-- ResourceLoader --------+
      +-- InspectLoader          |
           +-- ExecutionLoader --+
                                 +-- FileLoader
                                 +-- SourceLoader

通过查看源码可知,不同的加载器的抽象方法各有不同。

加载器通常由一个 finder 返回。详情参见 PEP 302,对于 abstract base class 可参见 importlib.abc.Loader。

那如何自定义我们自己的加载器呢?

你只要

  • 定义一个实现了 load_module 方法的类

  • 对与导入有关的属性(点击查看详情)进行校验

  • 创建模块对象并绑定所有与导入相关的属性变量到该模块上

  • 将此模块保存到 sys.modules 中(顺序很重要,避免递归导入)

  • 然后加载模块(这是核心)

  • 若加载出错,需要能够处理抛出异常( ImportError)

  • 若加载成功,则返回 module 对象

若你想看具体的例子,可以接着往后看。

3. 模块规格说明

导入机制在导入期间会使用有关每个模块的多种信息,特别是加载之前。 大多数信息都是所有模块通用的。 模块规格说明的目的是基于每个模块来封装这些导入相关信息。

模块的规格说明会作为模块对象的 __spec__ 属性对外公开。 有关模块规格的详细内容请参阅 `ModuleSpec <https://docs.python.org/zh-cn/3/library/importlib.html#importlib.machinery.ModuleSpec>`__。

在 Python 3.4 后,查找器不再返回加载器,而是返回 ModuleSpec 对象,它储存着更多的信息

  • 模块名

  • 加载器

  • 模块绝对路径

那如何查看一个模块的 ModuleSpec ?

这边举个例子

$ cat my_mod02.py
import my_mod01
print(my_mod01.__spec__)

$ python3 my_mod02.py
in mod01
ModuleSpec(name='my_mod01', loader=<_frozen_importlib_external.SourceFileLoader object at 0x000000000392DBE0>, origin='/home/MING/my_mod01.py')

从 ModuleSpec 中可以看到,加载器是包含在内的,那我们如果要重新加载一个模块,是不是又有了另一种思路了?

来一起验证一下。

现在有两个文件:

一个是 my_info.py

# my_info.py
name='wangbm'

另一个是:main.py

# main.py
import my_info

print(my_info.name)

# 加一个断点
import pdb;pdb.set_trace()

# 再加载一次
my_info.__spec__.loader.load_module()

print(my_info.name)

main.py 处,我加了一个断点,目的是当运行到断点处时,我修改 my_info.py 里的 name 为 ming ,以便验证重载是否有效?

$ python3 main.py
wangbm
> /home/MING/main.py(9)<module>()
-> my_info.__spec__.loader.load_module()
(Pdb) c
ming

从结果来看,重载是有效的。

4. 导入器是什么?

导入器(importer),也许你在其他文章里会见到它,但其实它并不是个新鲜的东西。

它只是同时实现了查找器和加载器两种接口的对象,所以你可以说导入器(importer)是查找器(finder),也可以说它是加载器(loader)。