书接上回,我们已经知道如何调试大部分的冻结模块,然而,由于importlib
的_bootstrap
属性是通过导入_frozen_importlib
这个不同名的冻结模块实现的,前文的方法仍无法做到对importlib._bootstrap
的源码级调试,需要进一步研究如何通过patch的方式,诱拐解释器载入源码实现调试。
importlib._bootstrap
的导入实现
上文说到,importlib._bootstrap
是importlib
的一个属性,本质上指向的是一个名为_frozen_importlib
的冻结模块。与此同时,我们发现Python提供的标准库(目录Lib
)中,importlib
包里面有一个_bootstrap.py
文件,其就是_frozen_importlib
的源码。
importlib
包体包含_bootstrap.py
文件
因此问题在于,对于importlib._bootstrap
,我们怎样引导Python解释器载入importlib
包的_bootstrap.py
源码文件,而非_frozen_importlib
冻结模块?
我们来看看importlib
的实现,观察importlib._bootstrap
是如何导入的:
# importlib.__init__.py的部分源码
import _imp # Just the builtin component, NOT the full Python module
import sys
try:
import _frozen_importlib as _bootstrap
except ImportError:
from . import _bootstrap
_bootstrap._setup(sys, _imp)
else:
# importlib._bootstrap is the built-in import, ensure we don't create
# a second copy of the module.
_bootstrap.__name__ = 'importlib._bootstrap'
_bootstrap.__package__ = 'importlib'
try:
_bootstrap.__file__ = __file__.replace('__init__.py', '_bootstrap.py')
except NameError:
# __file__ is not guaranteed to be defined, e.g. if this code gets
# frozen by a tool like cx_Freeze.
pass
sys.modules['importlib._bootstrap'] = _bootstrap
# ...
可以看到,importlib
先尝试直接通过import _frozen_importlib
的方式导入冻结模块,如果导入成功,则执行else
后面的语句块,使得_frozen_importlib
模块能冒充成importlib._bootstrap
子模块。如果导入_frozen_importlib
失败,则importlib
会回退到导入_bootstrap.py
源码,这也是我们所希望的方式,导入源码就意味着调试器也能对其进行源码级的调试!
诱导Python重新导入importlib._bootstrap
和importlib._bootstrap_external
的源码版本
在本节中,我们通过importlib.import_module
方法导入importlib._bootstrap
源码,并改变import
语句的入口点,指向importlib._bootstrap
非冻结版本的__import__
实现。此外,我们还顺便导入importlib._bootstrap_external
的源码版本。
def _patch_importlib_bootstrap():
import sys
import builtins
import importlib
import _imp
# 首先删除掉sys.modules里相关的模块,以便重新加载
del sys.modules['importlib._bootstrap']
del sys.modules['_frozen_importlib']
del sys.modules['_frozen_importlib_external']
# 加载importlib._bootstrap的源码
boostrap_py_module = importlib.import_module('importlib._bootstrap')
# 模仿importlib的行为, 调用_setup方法初始化
boostrap_py_module._setup(sys, _imp)
# 重新设置sys.modules, 使得importlib._bootstrap指向新的、源码加载的模块
sys.modules['importlib._bootstrap'] = importlib._bootstrap = boostrap_py_module
# !!! 注意这里, 我们也需要将import语句的入口设置为boostrap_py_module.__import__
builtins.__import__ = importlib.__import__ = boostrap_py_module.__import__
# 同上, 加载_frozen_importlib_external的源码
boostrap_external_module = importlib.import_module('_frozen_importlib_external')
boostrap_external_module._set_bootstrap_module(boostrap_py_module)
sys.modules['_frozen_importlib_external'] = _frozen_importlib_external = boostrap_external_module
_patch_importlib_bootstrap()
上述代码主要有以下关键点:
-
我们先删掉
sys.modules
中相关的模块,避免后续的导入流程直接使用缓存; -
其次,我们通过
importlib.import_module('importlib._bootstrap')
的方式,尝试通过源码导入importlib._bootstrap
模块。为什么这里就能够导入非冻结版本呢?我们在上面已经提到,importlib._bootstrap
实际上是importlib
的属性,其在importlib
的初始化过程中指向的一个不同名的冻结模块_frozen_importlib
。注意到该函数中,我们并没有删掉importlib
的模块缓存,也就是说,importlib._bootstrap
并不会重新触发importlib.__init__.py
里的代码。此时importlib._bootstrap
判断父模块importlib
已经在缓存中,从而以importlib
所在路径作为搜索入口,检索名为_bootstrap
的模块,因此就定位到文件_bootstrap.py
,从而加载的是源码版本。注:如果我们删除了
importlib
的模块缓存(即sys.modules.pop('importlib', None)
),调用importlib.import_module('importlib._bootstrap')
将会先导入importlib
,此时如上文所述,importlib
的初始化过程会导入冻结模块_frozen_importlib
作为其_bootstrap
属性,并加入到sys.modules
中,而后,importlib.import_module
发现sys.modules
已经存在importlib._boostrap
,于是不会继续后面的导入操作!这个是Python导入机制的一个副作用,具体可以看先前的这篇文章。 -
最后,我们还需要将非冻结版本的
importlib._bootstrap
模块加入到缓存sys.modules
中,并将builtins.__import__
和importlib.__import__
均指向非冻结版本的importlib._bootstrap
模块里的__import__
方法。否则,后续的import
语句仍使用之前的冻结版本。
最后,我们尝试下调试import
语句,看看能不能进去到导入系统里面。从下面的动图可以发现,我们先往import PyQt5
语句打上断点,命中后并成功Step Into里面的源码,且成功触发了_bootstrap.py
源码里的断点,这说明我们的补丁方法成功做到了对importlib._bootstrap
的源码级调试。
补丁后,成功对importlib._bootstrap
进行源码级的调试
总结
在本文中,我们基于上一篇文章,进一步研究了如何通过补丁的方式实现对importlib_bootstrap
模块的源码级调试,为我们后续对导入系统的研究打下了良好的基础。Let’s enjoy Python!