之前实现了python module多版本动态加载,但是在最近一次一个小哥上传的模型中居然有对.so
文件的import,十万只草泥马,Python居然还有这种操作,所以再一次对Python的import流程做了一次梳理。
python import的核心实现都在_bootstrap.py
中,很郁闷的是明明python lib下有_bootstrap.py
源码,但是eclipse+pydev
就是Debug不进去,无奈只好再开个eclipse
(还好有两个显示屏),导入python3.6.3的源码查看。
虽然Eclipse+pydev
无法跳入python底层的一些源码,但是debug调试栈还是可以看见的,所以通过单步调试加上下文变量查看窗口,再结合源码,也可以进行跟踪,虽然麻烦点。
先看一下我们要跟踪代项目代码结构:
|- pth_nms.py
|- _ext
|- __init__.py
|- nms
|- __init__.py
|- _nms.so
我们再大致看一下pth_nms.py
和_ext.nms.__init__.py
的部分代码,有个概念:
# pth_nms.py
from ._ext import nms
def pth_nms(dets, thresh):
...
# _ext.nms.__init__.py
from torch.utils.ffi import _wrap_function
from ._nms import lib as _lib, ffi as _ffi
__all__ = []
def _import_symbols(locals):
for symbol in dir(_lib):
fn = getattr(_lib, symbol)
if callable(fn):
locals[symbol] = _wrap_function(fn, _ffi)
else:
locals[symbol] = fn
__all__.append(symbol)
_import_symbols(locals())
这段代码还是挺有意思的,__init__.py
中解析了_nms.so
中的方法,然后放入_ext.nms
的__all__
属性中,这样后面想用_nms.so
中的方法,可以通过nms
这个module进行限定,例如:
from ._ext import nms
nms.cpu_nms(xx,xx,xx) #cpu_nms为_nms.so中的一个方法
在这个例子中我们的重点放在:
from ._ext import nms
from ._nms import lib as _lib, ffi as _ffi
这两行代码,我们先将断点放在from ._ext import nms
上,然后debug一步一步看加载的流程!
Python Import
-------------------------------------- _ext 查找加载 --------------------------------------
_bootstrap._find_and_load()
def _find_and_load(name, import_):
"""Find and load the module."""
with _ModuleLockManager(name):
module = sys.modules.get(name, _NEEDS_LOADING)
if module is _NEEDS_LOADING:
return _find_and_load_unlocked(name, import_)
if module is None:
message = ('import of {} halted; '
'None in sys.modules'.format(name))
raise ModuleNotFoundError(message, name=name)
_lock_unlock_module(name)
return module
这里有一个比较有意思的python特性,就是with语法,with语法块会将代码块的进入和退出通知被with的对象,如上面with _ModuleLockManager(name)
,当进入代码块时,会通知_ModuleLockManager
的__enter__
方法,退出时通知__exit__
方法。
_ModuleLockManager
是module锁管理对象,使用with特性可以很好进行锁的申请、释放管理。贴一下_ModuleLockManager
源码:
class _ModuleLockManager:
def __init__(self, name):
self._name = name
self._lock = None
def __enter__(self):
self._lock = _get_module_lock(self._name)
self._lock.acquire()
def __exit__(self, *args, **kwargs):
self._lock.release()
回到正题,继续_find_and_load
,该函数接受两个参数,进过跟踪变量值为:
name='_ext'
import_
为内置函数__import__
_find_and_load
主要逻辑为从sys.modules
查找该名称是否已经存在加载过的module,如果存在则返回,不存在则进行查找和加载;存在且module为None的话。sys.modules
缓存了所有已经被加载的module。
_bootstrap._find_and_load_unlocked()
同样先贴出源码:
def _find_and_load_unlocked(name, import_):
path = None
parent = name.rpartition('.')[0]
if parent:
if parent not in sys.modules:
_call_with_frames_removed(import_, parent)
# Crazy side-effects!
if name in sys.modules:
return sys.modules[name]
parent_module = sys.modules[parent]
try:
path = parent_module.__path__
except AttributeError:
msg = (_ERR_MSG + '; {!r} is not a package').format(name, parent)
raise ModuleNotFoundError(msg, name=name) from None
spec = _find_spec(name, path)
if spec is None:
raise ModuleNotFoundError(_ERR_MSG.format(name), name=name)
else:
module = _load_unlocked(spec)
if parent:
# Set the module as an attribute on its parent.
parent_module = sys.modules[parent]
setattr(parent_module, name.rpartition('.')[2], module)
return module
传入的变量值就是_find_and_load
的两个变量,未做任何修改。该方法主要逻辑为:查找该name对应module的父module,然后得到父module的path路径,再根据父module的路径找我们要找的module。找到之后,我们则需要进行module的加载。
也就是说,到这里之后,我们的事情就变成了两件事:
- 查找
- 加载
在Python中,查找和加载主要对应Finder(查找器)、Loader(加载器),顺着例子继续看:
Finder
开启查找的第一步:
_bootstrap._find_spec()
def _find_spec(name, path, target=None):
"""Find a module's spec."""
meta_path = sys.meta_path
if meta_path is None:
# PyImport_Cleanup() is running or has been called.
raise ImportError("sys.meta_path is None, Python is likely "
"shutting down")
if not meta_path:
_warnings.warn('sys.meta_path is empty', ImportWarning)
# We check sys.modules here for the reload case. While a passed-in
# target will usually indicate a reload there is no guarantee, whereas
# sys.modules provides one.
is_reload = name in sys.modules
for finder in meta_path:
with _ImportLockContext():
try:
find_spec = finder.find_spec
except AttributeError:
spec = _find_spec_legacy(finder, name, path)
if spec is None:
continue
else:
spec = find_spec(name, path, target)
if spec is not None:
# The parent import may have already imported this module.
if not is_reload and name in sys.modules:
module = sys.modules[name]
try:
__spec__ = module.__spec__
except AttributeError:
# We use the found spec since that is the one that
# we would have used if the parent module hadn't
# beaten us to the punch.
return spec
else:
if __spec__ is None:
return spec
else:
return __spec__
else:
return spec
else:
return None
该方法主要逻辑为遍历所有sys.meta_path
,通过sys.meta_path
中的定义的Importer/Finder进行moudle查找和加载实现规范,也就是说,如果我们想自己实现module加载可以实现Importer/Finder,然后放入sys.meta_path
中.我们看一下sys.meta_path
有哪些Importer/Finder:
BuiltinImporter
FrozenImporter
PathFinder
pkg_resources.extern.VendorImporter
pkg_resources._vendor.six._SixMetaPathImporter
six._SixMetaPathImporter
因为_ext
存在OS路径中,所以这里势必使用的是PathFinder,开头我们说到_bootstrap.py
是python import的核心实现,而PathFinder
类对应的_bootstrap_external.py
则是基于路径import的核心实现。
_bootstrap._find_spec()
中最关键的代码就是尝试使用不同的Importer/Finder查找module,这里我们直接定位调用PathFinder
类的find_spec
方法。
_bootstrap_external.PathFinder.find_spec()
@classmethod
def find_spec(cls, fullname, path=None, target=None):
"""Try to find a spec for 'fullname' on sys.path or 'path'.
The search is based on sys.path_hooks and sys.path_importer_cache.
"""
if path is None:
path = sys.path
spec = cls._get_spec(fullname, path, target)
if spec is None:
return None
elif spec.loader is None:
namespace_path = spec.submodule_search_locations
if namespace_path:
# We found at least one namespace path. Return a
# spec which can create the namespace package.
spec.origin = 'namespace'
spec.submodule_search_locations = _NamespacePath(fullname, namespace_path, cls._get_spec)
return spec
else:
return None
else:
return spec
这里有几个入参:
fullname='_ext'
,即module完整的名称。path
为_ext
父module的系统路径。- target为None
该函数核心为调用_get_spec()
,参数不变的调用。这里的path不为none,但是如果为None则使用sys.path
定义的路径,举个例子,site-packages下的module能够被发现,就是因为site-packages路径默认是追加在sys.path
中的。这也给我们一个启发,除了自定义Importer/Loader放入sys.meta_path
,通过加要加载module基础路径放入sys.path
中来快速实现module的查找和加载。
_bootstrap_external.PathFinder._get_spec()
@classmethod
def _get_spec(cls, fullname, path, target=None):
"""Find the loader or namespace_path for this module/package name."""
# If this ends up being a namespace package, namespace_path is
# the list of paths that will become its __path__
namespace_path = []
for entry in path:
if not isinstance(entry, (str, bytes)):
continue
finder = cls._path_importer_cache(entry)
if finder is not None:
if hasattr(finder, 'find_spec'):
spec = finder.find_spec(fullname, target)
else:
spec = cls._legacy_get_spec(fullname, finder)
if spec is None:
continue
if spec.loader is not None:
return spec
portions = spec.submodule_search_locations
if portions is None:
raise ImportError('spec missing loader')
# This is possibly part of a namespace package.
# Remember these path entries (if any) for when we
# create a namespace package, and continue iterating
# on path.
namespace_path.extend(portions)
else:
spec = _bootstrap.ModuleSpec(fullname, None)
spec.submodule_search_locations = namespace_path
return spec
该函数遍历给定的路径,然后根据不同路径会给出不同的Finder,我们可以看一下_path_importer_cache
的实现:
@classmethod
def _path_importer_cache(cls, path):
"""Get the finder for the path entry from sys.path_importer_cache.
If the path entry is not in the cache, find the appropriate finder
and cache it. If no finder is available, store None.
"""
if path == '':
try:
path = _os.getcwd()
except FileNotFoundError:
# Don't cache the failure as the cwd can easily change to
# a valid directory later on.
return None
try:
finder = sys.path_importer_cache[path]
except KeyError:
finder = cls._path_hooks(path)
sys.path_importer_cache[path] = finder
return finder
_path_importer_cache
优先从cache看有没有对应处理该路径的Findler,没有则调用_path_hooks
找可以处理该路径的Finder,再看看_path_hooks()
:
@classmethod
def _path_hooks(cls, path):
"""Search sys.path_hooks for a finder for 'path'."""
if sys.path_hooks is not None and not sys.path_hooks:
_warnings.warn('sys.path_hooks is empty', ImportWarning)
for hook in sys.path_hooks:
try:
return hook(path)
except ImportError:
continue
else:
return None
_path_hooks()
遍历sys.path_hooks
中注册的路径钩子函数,如果有一个钩子函数能够处理给定的路径,并且返回Findler,则使用该Finder进行module的查找加载。看到这里我们发现,自定义module加载又多了一种方式,在我们的系统中实现module多版本动态加载,就是通过注册路径钩子函数进行实现的,如果想通过定义钩子函数实现动态加载的,可以参考我的另外一篇文章Python多版本多module动态加载
回到_bootstrap_external.PathFinder._get_spec()
的主逻辑,找到对应的Finder之后,调用Finder的find_spec()
。
sys.path_hooks
中注册的钩子函数返回的Finder和从sys.meta_path
拿出来的Finder是有区别的,在这个例子中sys.meta_path
给出的是PathFinder,sys.path_hooks
给出的是FileFinder。
_bootstrap_external.FileFinder.find_spec()
这个里面代码比较多,就不在多贴了,大致说一下,该方法判断给定的名称是package还是正常的py,并给出用于该module加载使用的loader,最后返回ModuleSpec对象,该对象作为moudle加载的规范,用于后面module的加载。ModuleSpec是module的源信息,主要包含以下属性:
- name ,module的绝对名称,
- loader,用于module加载的加载器
- is_package,该name对应的module是否为一个包
- origin,module的路径,用于加载器从哪加载使用的位置
- submodule_search_locations,用于加载该module下的子module时的查找路径。
- parent ,module所在的包名
该例子执行到这步,返回的ModuleSpec对象内容大致如下:
name
:'_ext'
loader
:SourceFileLoader: <_frozen_importlib_external.SourceFileLoader object at 0x7f2df3838e80>
origin
:'/home/.../_ext/__init__.py'
submodule_search_locations
:['/home/.../_ext']
parent
:_ext
其实成功返回了ModuleSpec,就意味着已经完成了python finder的流程,也就是Finder所干的事情!
对应这里我们找到了_ext
这个module,实际上的是_ext.__init__.py
,下面就是要对_ext.__init__.py
进行加载。
Loader
回到_bootstrap._find_and_load_unlocked()
,看对spec的加载,加载实际上就是ModuleSpec到module的过程。
_bootstrap._load_unlocked
def _load_unlocked(spec):
# A helper for direct use by the import system.
if spec.loader is not None:
# not a namespace package
if not hasattr(spec.loader, 'exec_module'):
return _load_backward_compatible(spec)
module = module_from_spec(spec)
with _installed_safely(module):
if spec.loader is None:
if spec.submodule_search_locations is None:
raise ImportError('missing loader', name=spec.name)
# A namespace package so do nothing.
else:
spec.loader.exec_module(module)
# We don't ensure that the import-related module attributes get
# set in the sys.modules replacement case. Such modules are on
# their own.
return sys.modules[spec.name]
该函数通过调用module_from_spec
将ModuleSpec转换为module,我们先看module_from_spec
:
_bootstrap.module_from_spec()
def module_from_spec(spec):
"""Create a module based on the provided spec."""
# Typically loaders will not implement create_module().
module = None
if hasattr(spec.loader, 'create_module'):
# If create_module() returns `None` then it means default
# module creation should be used.
module = spec.loader.create_module(spec)
elif hasattr(spec.loader, 'exec_module'):
raise ImportError('loaders that define exec_module() '
'must also define create_module()')
if module is None:
module = _new_module(spec.name)
_init_module_attrs(spec, module)
return module
该函数创建空的module对象,然后对module进行初始化,初始化包含对module的如下属性进行赋值:
- name,module名称
- loader,加载器
- package,包名称,即ModuleSpec.parent
- spec,ModuleSpec对象
- path , module所在的路径,使用ModuleSpec.submodule_search_locations即可
- file,module对应文件绝对路径。即ModuleSpec.origin
完成对module参数初始化,我们再次回到_bootstrap._load_unlocked
中,下面做的事情就是module的loader,看一下spec.loader.exec_module(module)
实现:
def exec_module(self, module):
"""Execute the module."""
code = self.get_code(module.__name__)
if code is None:
raise ImportError('cannot load module {!r} when get_code() '
'returns None'.format(module.__name__))
_bootstrap._call_with_frames_removed(exec, code, module.__dict__)
顺便贴出_bootstrap._call_with_frames_removed
:
def _call_with_frames_removed(f, *args, **kwds):
"""remove_importlib_frames in import.c will always remove sequences
of importlib frames that end with a call to this function
Use it instead of a normal call in places where including the importlib
frames introduces unwanted noise into the traceback (e.g. when executing
module code)
"""
return f(*args, **kwds)
其实事情比较简单,modul的所有信息都有了,就是读取source源码生成code对象,然后调用python的内置函数exec
对code进行执行,到这里就完成了module的加载执行。
其实到这里我们只完成了from .ext import nms
中_ext
的加载, 在加载执行完_ext.__init__.py
之后,我们还是会返回到from .ext import nms
断点。
-------------------------------------- nms 查找加载 --------------------------------------
还是要继续对nms
进行加载,对于nms
加载,从_bootstrap._handle_fromlist()
方法进入:
def _handle_fromlist(module, fromlist, import_):
if hasattr(module, '__path__'):
if '*' in fromlist:
fromlist = list(fromlist)
fromlist.remove('*')
if hasattr(module, '__all__'):
fromlist.extend(module.__all__)
for x in fromlist:
if not hasattr(module, x):
from_name = '{}.{}'.format(module.__name__, x)
try:
_call_with_frames_removed(import_, from_name)
except ModuleNotFoundError as exc:
# Backwards-compatibility dictates we ignore failed
# imports triggered by fromlist for modules that don't
# exist.
if exc.name == from_name:
continue
raise
return module
先给出在我们的例子中这几个参数的值:
- module,
module: <module '_ext' from '/home/.../_ext/__init__.py'
- fromlist,
<class 'tuple'>: ('nms',)
- import_,
builtin_function_or_method: <built-in function __import__>
对于from _ext import nms
实际要加载的是_ext.nms
。执行_call_with_frames_removed()
实际上执行:
__import__(('utils.lib.nms._ext.nms',),{})
最后还是逃避不了我们之前梳理出来的那一套Finder、Loader流程。也是还会回到_bootstrap._find_and_load()
开始对_ext.nms
查找和加载,通过前面的介绍,Finder返回ModuleSpec对象,Loader将ModuleSpec转为module对象,然后源码转为code进行执行,前面已经大篇幅给出了流程,后面不再赘述。因为_ext.nms
实际也是包,所以加载的应该是_ext.nms.__init__.py
。
Finder阶段返回的ModuleSpec为:
- loader,
SourceFileLoader: <_frozen_importlib_external.SourceFileLoader object at 0x7f2df38385f8>
- name,
_ext.nms
- origin,
/home/.../_ext/nms/__init__.py
- parent,
_ext.nms
- submodule_search_locations,
['/home/.../_ext/nms']
Loader阶段得到的Module对象为:
module: <module '_ext.nms' from '/home/.../_ext/nms/__init__.py'>
- name,
_ext.nms
- package,
_ext.nms
- path ,
['/home/.../_ext/nms']
- file,
/home/.../_ext/nms/__init__.py
-------------------------------------- nms 查找加载 --------------------------------------
在exec _ext.nms.__init__.py
时,遇到了要对.so加载的需求,也就是:
from ._nms import lib as _lib, ffi as _ffi
其实总的Finder、Loader流程都是一致的,最大不同在于FileFinder中,针对.so文件会创建和.py不同的Loader。
其实FileFinder针对不同文件给不同Loader是在初始化就决定的,我们看一下代码:
def __init__(self, path, *loader_details):
"""Initialize with the path to search on and a variable number of
2-tuples containing the loader and the file suffixes the loader
recognizes."""
loaders = []
for loader, suffixes in loader_details:
loaders.extend((suffix, loader) for suffix in suffixes)
self._loaders = loaders
*loader_details
参数已经定义好不同后缀文件对应的Loader。之前介绍过FileFinder是由路径钩子函数创建的,刚好在FileFinder系统定义了FileFinder能够处理路径的钩子函数,顺着这个找,我们找到了_bootstrap_external._get_supported_file_loaders()
:
def _get_supported_file_loaders():
"""Returns a list of file-based module loaders.
Each item is a tuple (loader, suffixes).
"""
extensions = ExtensionFileLoader, _imp.extension_suffixes()
source = SourceFileLoader, SOURCE_SUFFIXES
bytecode = SourcelessFileLoader, BYTECODE_SUFFIXES
return [extensions, source, bytecode]
source
和bytecode
对应的后缀分别为.py
和.pyc
,那么只有extensions
是能够处理.so
文件的,事实证明,确实是它。
找到了_nms.so
路径和加载它的ExtensionFileLoader,后面的事情就是回到_bootstrap._find_and_load_unlocked()
中对创建的ModuleSpec进行load,依旧是执行Loader类的exec_module()
,对于.py
文件底层最终会调用'exec'指令完成module的加载,而对于.so
实际调用的是内置的_imp.exec_dynamic
函数。
小结
Python的module加载核心就两个:
- 查找器(Finder)
- 加载器(Loader)
查找器目的在于找到要在家module的路径等信息,并为其绑定对应的加载器,最终封装成ModuleSpec返回,对于package找的是package下的__init__.py,而如果在查找过程中发现对应的父模块还未加载,那么会先去加载父module,这里面踩过一个坑,算了,不多说了,想知道的关注另外一篇文章:Python多版本多module动态加载.
查找器有两种,一种是通过sys.meta_path
绑定,还有一种是通过路径构建函数进行绑定。如果需要自定义module的查找和加载,可以考虑这两点。
加载器就是完成对module的读取,然后执行。