已经隔了好久没有更新文章了🙄🙄,今天我们来学习一下 Python import 的实现原理吧,Python 分别可以通过 from xxx import xxx 与 import xxx 来进行导入模块,我们慢慢来分析一下这两种实现原理。
# Python from xxx import xxx 实现原理
基于 CPython3.9.12 版本阅读
关于 import 相关使用可以参考菜鸟进行了解,都是比较基础的知识:https://www.runoob.com/python/python-modules.html
以下分析均为个人理解,如有错误可以提供指正
首先 Python 在执行时,会将代码转为字节码,然后在 C 的解释器里边跑,那么我们如何知道 from xxx import xxx 这种在底层执行时,是在哪里进行调用的,我们最简单可以通过 cmd 起一个 python 交互,然后导入 dis 模块,通过 dis 模块对 Python 代码进行反汇编,就可以将代码转换为字节码指令(可读形式)。
>>> import dis | |
>>> dis.dis("from hashlib import md5") | |
1 0 LOAD_CONST 0 (0) | |
2 LOAD_CONST 1 (('md5',)) | |
4 IMPORT_NAME 0 (hashlib) | |
6 IMPORT_FROM 1 (md5) | |
8 STORE_NAME 1 (md5) | |
10 POP_TOP | |
12 LOAD_CONST 2 (None) | |
14 RETURN_VALUE | |
>>> |
先解释一下每列的作用是什么
- 第 1 列:偏移量,表示每条指令在字节码序列中的位置
- 第 2 列:操作码,表示所执行的动作(可以通过操作码在 CPython 的 ceval.c 文件中找到对应操作)
- 第 3 列:操作数,它提供了指令所需的额外信息。操作数的含义取决于操作码,例如第一行,LOAD_CONST 0 (0),操作数是一个常量索引;IMPORT_NAME (hashlib),操作数是一个字符串,表示要导入的模块名称
接下来进行每行解读:
LOAD_CONST 0 (0):加载一个常量,将索引为 0 的常量(在常量池中的第一个常量)加载到堆栈上,它加载的是整数 0
LOAD_CONST 1 ((‘md5’,)):加载一个常量,将索引为 1 的常量加载到堆栈上,但在这里,它加载的是一个包含单个字符串元组 (‘md5’,)
case TARGET(LOAD_CONST): { | |
PREDICTED(LOAD_CONST); | |
/** | |
consts 是常量池,oparg 也就是索引,例如 LOAD_CONST 1 (('md5',)),相当于执行了 consts [opargs] | |
GETITEM 调用了 PyTuple_GetItem 来获取执行索引位置的元素 | |
**/ | |
PyObject *value = GETITEM(consts, oparg); | |
Py_INCREF(value); // 引用计数 + 1 | |
PUSH(value); // 将获取的元素压到栈顶 | |
FAST_DISPATCH(); // 执行下一个字节码 | |
} |
IMPORT_NAME 0 (hashlib) 实现方法:
case TARGET(IMPORT_NAME): { | |
// 通过 GETITEM 在 names 中获取 hashlib 字符串 | |
PyObject *name = GETITEM(names, oparg); | |
PyObject *fromlist = POP(); // 从栈中弹出一个元素,也就是 md5 字符串 | |
PyObject *level = TOP(); // 从栈中弹出顶部元素 | |
PyObject *res; | |
res = import_name(tstate, f, name, fromlist, level); // 主要实现,我们来看看做了什么 | |
Py_DECREF(level); | |
Py_DECREF(fromlist); | |
SET_TOP(res); // 根据下面的代码解析知道 res 是一个 module object, 然后通过 SET_TOP 将栈顶的值设置为 res | |
if (res == NULL) | |
goto error; | |
DISPATCH(); // continue | |
} |
import_name 方法实现
static PyObject * | |
import_name(PyThreadState *tstate, PyFrameObject *f, | |
PyObject *name, PyObject *fromlist, PyObject *level) | |
{ | |
// name: "hashlib"、fromlist: ('md5',)、level: 0 | |
_Py_IDENTIFIER(__import__); | |
PyObject *import_func, *res; | |
PyObject* stack[5]; | |
// 从内置的 dict 中获取__import__对象 | |
import_func = _PyDict_GetItemIdWithError(f->f_builtins, &PyId___import__); | |
if (import_func == NULL) { | |
if (!_PyErr_Occurred(tstate)) { | |
_PyErr_SetString(tstate, PyExc_ImportError, "__import__ not found"); | |
} | |
return NULL; | |
} | |
/* Fast path for not overloaded __import__. */ | |
if (import_func == tstate->interp->import_func) { | |
/** | |
import_func 是默认的__import__函数,而 tstate->interp->import_func 是解释器状态中默认的__import__函数引用, | |
如果没有对__import__进行自定义重载的话,并且__import__函数与解释器默认的__import__函数相同,那么代码将会走这个 fast path, | |
直接调用默认的__import__函数来执行模块导入操作,而不会调用自定义的实现。 | |
fast path 的目的是为了避免在默认情况下产生额外的开销,基本上都会进入该 if statement | |
**/ | |
int ilevel = _PyLong_AsInt(level); | |
if (ilevel == -1 && _PyErr_Occurred(tstate)) { | |
// 异常捕获 | |
return NULL; | |
} | |
// 主要执行该方法,我们来看看 | |
res = PyImport_ImportModuleLevelObject( | |
name, | |
f->f_globals, | |
f->f_locals == NULL ? Py_None : f->f_locals, | |
fromlist, | |
ilevel); | |
return res; | |
} | |
Py_INCREF(import_func); | |
stack[0] = name; | |
stack[1] = f->f_globals; | |
stack[2] = f->f_locals == NULL ? Py_None : f->f_locals; | |
stack[3] = fromlist; | |
stack[4] = level; | |
res = _PyObject_FastCall(import_func, stack, 5); | |
Py_DECREF(import_func); | |
return res; | |
} |
PyImport_ImportModuleLevelObject 的代码比较长,我会忽略一些不重要的代码
PyObject * | |
PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals, | |
PyObject *locals, PyObject *fromlist, | |
int level) | |
{ | |
//name 是 hashlib 字符串,fromlist 是 ('md5',), level: 0 | |
PyThreadState *tstate = _PyThreadState_GET(); | |
_Py_IDENTIFIER(_handle_fromlist); | |
PyObject *abs_name = NULL; | |
PyObject *final_mod = NULL; | |
PyObject *mod = NULL; | |
PyObject *package = NULL; | |
PyInterpreterState *interp = tstate->interp; | |
int has_from; | |
// ...... | |
if (level > 0) { | |
abs_name = resolve_name(tstate, name, globals, level); | |
if (abs_name == NULL) | |
goto error; | |
} | |
else { /* level == 0 */ | |
if (PyUnicode_GET_LENGTH(name) == 0) { | |
_PyErr_SetString(tstate, PyExc_ValueError, "Empty module name"); | |
goto error; | |
} | |
// 因为 level 为 0, 所以将 abs_name 赋值为 hashlib 字符串 | |
abs_name = name; | |
Py_INCREF(abs_name); | |
} | |
// 从内置 modules 中获取 hashlib module 对象 | |
mod = import_get_module(tstate, abs_name); | |
if (mod == NULL && _PyErr_Occurred(tstate)) { | |
goto error; | |
} | |
// ...... | |
has_from = 0; | |
if (fromlist != NULL && fromlist != Py_None) { | |
has_from = PyObject_IsTrue(fromlist); | |
if (has_from < 0) | |
goto error; | |
} | |
if (!has_from) { | |
// ...... | |
} | |
else { | |
// 进入这里 | |
PyObject *path; | |
// 通过_PyObject_LookupAttrId 在模块对象中查找__path__属性 | |
if (_PyObject_LookupAttrId(mod, &PyId___path__, &path) < 0) { | |
goto error; | |
} | |
if (path) { | |
// 如果成功找到了__path__属性 | |
Py_DECREF(path); //path 引用计数 - 1,不再需要 path 属性的引用 | |
/** | |
_PyObject_CallMethodIdObjArgs 方法只要做了:在 importlib 中查找_handle_fromlist 方法,并调用 | |
_handle_fromlist (mod, fromlist, interp->import_func) 作为参数传递,NULL 用于结束参数列表, | |
在下方会粘贴_handle_fromlist 的实现。 | |
**/ | |
final_mod = _PyObject_CallMethodIdObjArgs( | |
interp->importlib, &PyId__handle_fromlist, | |
mod, fromlist, interp->import_func, NULL); | |
} | |
else { | |
final_mod = mod; | |
Py_INCREF(mod); | |
} | |
} | |
error: | |
Py_XDECREF(abs_name); | |
Py_XDECREF(mod); | |
Py_XDECREF(package); | |
if (final_mod == NULL) { | |
remove_importlib_frames(tstate); | |
} | |
return final_mod; | |
} |
def _handle_fromlist(module, fromlist, import_, *, recursive=False): | |
# module 就是 hashlib module 对象、fromlist 就是 ('md5',) | |
for x in fromlist: | |
if not isinstance(x, str): | |
# 如果不是 str 类型,抛异常 | |
if recursive: | |
where = module.__name__ + '.__all__' | |
else: | |
where = "``from list''" | |
raise TypeError(f"Item in {where} must be str, " | |
f"not {type(x).__name__}") | |
elif x == '*': | |
# 如果导入所有,即 * 号 | |
if not recursive and hasattr(module, '__all__'): # 本次调用不是通过递归方法,且判断是否定义__all__属性 | |
# 将当前 module 对象的__all__传递进去,也就是下次将迭代的 fromlist、recursive 告诉方法本次是递归调用 | |
_handle_fromlist(module, module.__all__, import_, | |
recursive=True) | |
elif not hasattr(module, x): | |
# 如果模块对象中没有该属性 | |
from_name = '{}.{}'.format(module.__name__, x) | |
try: | |
# 调用内置的_import__方法,传递 from_name | |
_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 and | |
sys.modules.get(from_name, _NEEDS_LOADING) is not None): | |
continue | |
raise | |
# 返回 module object | |
return module |
6 IMPORT_FROM 1 (md5):实现方法
case TARGET(IMPORT_FROM): { | |
// 获取 md5 字符串 | |
PyObject *name = GETITEM(names, oparg); | |
// 获取栈顶的元素,也就是在 IMPORT_NAME 中通过 SET_TOP 设置的 res | |
PyObject *from = TOP(); | |
PyObject *res; | |
res = import_from(tstate, from, name); // 只要处理方法 | |
PUSH(res); // 将 res 推到栈中 | |
if (res == NULL) | |
goto error; | |
DISPATCH(); // continue | |
} |
import_from 实现方法
static PyObject * | |
import_from(PyThreadState *tstate, PyObject *v, PyObject *name) | |
{ | |
//v: hashlib module object、name: md5 字符串 | |
PyObject *x; | |
PyObject *fullmodname, *pkgname, *pkgpath, *pkgname_or_unknown, *errmsg; | |
// 通过_PyObject_LookupAttr 在 hashlib module object 中查找 md5 属性并返回 | |
//_PyObject_LookupAttr 方法做了什么可以看我往期剖析 getattr 的实现原理,有讲到此方法 | |
if (_PyObject_LookupAttr(v, name, &x) != 0) { | |
return x; | |
} | |
// 如果上面没有在 hashlib module object 中找到对应属性,则执行下面逻辑 | |
/* Issue #17636: in case this failed because of a circular relative | |
import, try to fallback on reading the module directly from | |
sys.modules. */ | |
pkgname = _PyObject_GetAttrId(v, &PyId___name__); | |
// 获取 hashlib 的__name__属性 | |
if (pkgname == NULL) { | |
goto error; | |
} | |
if (!PyUnicode_Check(pkgname)) { | |
// 检测是否为 str 类型 | |
Py_CLEAR(pkgname); | |
goto error; | |
} | |
// 拼接完整的模块名加属性名 -> hashlib.md5 | |
fullmodname = PyUnicode_FromFormat("%U.%U", pkgname, name); | |
if (fullmodname == NULL) { | |
Py_DECREF(pkgname); | |
return NULL; | |
} | |
/** | |
使用 hashlib.md5 从内置 modules 中查找模块,里面有个 import_ensure_initialized 方法,会对该模块进行一些初始化工作, | |
检查给定的模块对象是否已经被初始化。在多线程下,多线程可能会同时尝试初始化同一个模块,import_ensure_initialized 函数 | |
会考虑多线程安全,避免多次执行重复的初始化操作,以确保模块状态的一致性 (下方会展示代码)。另外,在导入模块时,可能会使用缓存 | |
机制来避免重复导入 | |
**/ | |
x = PyImport_GetModule(fullmodname); | |
Py_DECREF(fullmodname); // 引用计数 - 1 | |
if (x == NULL && !_PyErr_Occurred(tstate)) { | |
// 异常捕获 | |
goto error; | |
} | |
Py_DECREF(pkgname); | |
return x; // 返回模块对象 | |
error: | |
// 下面都是异常处理,不是重点 | |
pkgpath = PyModule_GetFilenameObject(v); | |
if (pkgname == NULL) { | |
pkgname_or_unknown = PyUnicode_FromString("<unknown module name>"); | |
if (pkgname_or_unknown == NULL) { | |
Py_XDECREF(pkgpath); | |
return NULL; | |
} | |
} else { | |
pkgname_or_unknown = pkgname; | |
} | |
if (pkgpath == NULL || !PyUnicode_Check(pkgpath)) { | |
_PyErr_Clear(tstate); | |
errmsg = PyUnicode_FromFormat( | |
"cannot import name %R from %R (unknown location)", | |
name, pkgname_or_unknown | |
); | |
/* NULL checks for errmsg and pkgname done by PyErr_SetImportError. */ | |
PyErr_SetImportError(errmsg, pkgname, NULL); | |
} | |
else { | |
_Py_IDENTIFIER(__spec__); | |
PyObject *spec = _PyObject_GetAttrId(v, &PyId___spec__); | |
const char *fmt = | |
_PyModuleSpec_IsInitializing(spec) ? | |
"cannot import name %R from partially initialized module %R " | |
"(most likely due to a circular import) (%S)" : | |
"cannot import name %R from %R (%S)"; | |
Py_XDECREF(spec); | |
errmsg = PyUnicode_FromFormat(fmt, name, pkgname_or_unknown, pkgpath); | |
/* NULL checks for errmsg and pkgname done by PyErr_SetImportError. */ | |
PyErr_SetImportError(errmsg, pkgname, pkgpath); | |
} | |
Py_XDECREF(errmsg); | |
Py_XDECREF(pkgname_or_unknown); | |
Py_XDECREF(pkgpath); | |
return NULL; | |
} |
import_ensure_initialized 在多线程下,避免多次执行重复的初始化操作
def _lock_unlock_module(name): | |
"""Acquires then releases the module lock for a given module name. | |
This is used to ensure a module is completely initialized, in the | |
event it is being imported by another thread. | |
""" | |
lock = _get_module_lock(name) | |
try: | |
lock.acquire() | |
except _DeadlockError: | |
# Concurrent circular import, we'll accept a partially initialized | |
# module object. | |
pass | |
else: | |
lock.release() |
经过 IMPORT_NAME、IMPORT_FROM 字节码,模块对应的属性已经被处理且推入栈中,接下来看看 STORE_NAME 做了什么操作让我门在当前作用域可以访问到导入的模块对象
8 STORE_NAME 1 (md5): 实现方法
case TARGET(STORE_NAME): { | |
// 在 names 中通过 oparg 索引获取 md5 字符串 | |
PyObject *name = GETITEM(names, oparg); | |
PyObject *v = POP(); // 弹出在 IMPORT_FROM 中 PUSH 的模块对象 | |
PyObject *ns = f->f_locals; // 获取当前 frame 的作用域 | |
int err; | |
if (ns == NULL) { | |
_PyErr_Format(tstate, PyExc_SystemError, | |
"no locals found when storing %R", name); | |
Py_DECREF(v); | |
goto error; | |
} | |
if (PyDict_CheckExact(ns)) | |
// 检测是否为 dict 类型,然后通过 PyDict_SetItem 给 locals 设置键值对 | |
err = PyDict_SetItem(ns, name, v); | |
else | |
// 不是一个 dict 类型,通过 PyObject_SetItem 给 locals 尝试使用魔法方法来进行赋值 | |
err = PyObject_SetItem(ns, name, v); | |
Py_DECREF(v); // 引用计数 - 1 | |
if (err != 0) | |
goto error; | |
DISPATCH(); // continue | |
} |
剩下的字节码指令(POP_TOP、LOAD_CONST、RETURN_VALUE)就是最后的结尾工作了,比如加载 None,然后 return None。from xxx import xxx 的大概流程基本了解了,首先通过内置 builtins 中,获取__import__,判断是否重载了__import__,在没有重载的情况下,会走快速通道,避免产生额外的调用开销,接着,通过内置的模块中获取被导入的模块对象并推到栈顶,然后从栈顶取出模块对象,并使用被 import 的字符串在模块对象中通过查找方法获取对应属性并设置到 locals 作用域中,在执行了 from xxx import xxx 时,本地作用域就可以访问到对应的方法了。
在 Python 中,当我们通过 from xxx import xxx 导入时,我们可以通过 locals 方法查询当前作用域有哪些对象
另外,当执行导入时,还有缓存机制优化,下次会直接通过 sys.modules 查找
# import 实现原理
import 的实现原理其实和 from xxx import xxx 差不多,我们来看下字节码
>>> dis.dis("import hashlib") | |
1 0 LOAD_CONST 0 (0) | |
2 LOAD_CONST 1 (None) | |
4 IMPORT_NAME 0 (hashlib) | |
6 STORE_NAME 1 (hashlib) | |
8 LOAD_CONST 1 (None) | |
10 RETURN_VALUE |
可以看到,没有了 IMPORT_FROM 指令,只有 IMPORT_NAME,所以当我们执行 import 进行导入时,我们实际上操作的是 moduleobject,而不再是模块对应的方法对象了
>>> from hashlib import md5 | |
>>> import hashlib | |
>>> md5 | |
<built-in function openssl_md5> | |
>>> hashlib | |
<module 'hashlib' from 'D:\\python3.9.12\\lib\\hashlib.py'> |
既然当我们通过 import 导入模块对象并操作时,我们是如果获取对应属性的呢?比如 hashlib.md5,我们先来看看 moduleobject 的结构
PyTypeObject PyModule_Type = { | |
PyVarObject_HEAD_INIT(&PyType_Type, 0) | |
"module", /* tp_name */ | |
sizeof(PyModuleObject), /* tp_basicsize */ | |
0, /* tp_itemsize */ | |
(destructor)module_dealloc, /* tp_dealloc */ | |
0, /* tp_vectorcall_offset */ | |
0, /* tp_getattr */ | |
0, /* tp_setattr */ | |
0, /* tp_as_async */ | |
(reprfunc)module_repr, /* tp_repr */ | |
0, /* tp_as_number */ | |
0, /* tp_as_sequence */ | |
0, /* tp_as_mapping */ | |
0, /* tp_hash */ | |
0, /* tp_call */ | |
0, /* tp_str */ | |
(getattrofunc)module_getattro, /* tp_getattro */ | |
PyObject_GenericSetAttr, /* tp_setattro */ | |
0, /* tp_as_buffer */ | |
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | | |
Py_TPFLAGS_BASETYPE, /* tp_flags */ | |
module___init____doc__, /* tp_doc */ | |
(traverseproc)module_traverse, /* tp_traverse */ | |
(inquiry)module_clear, /* tp_clear */ | |
0, /* tp_richcompare */ | |
offsetof(PyModuleObject, md_weaklist), /* tp_weaklistoffset */ | |
0, /* tp_iter */ | |
0, /* tp_iternext */ | |
module_methods, /* tp_methods */ | |
module_members, /* tp_members */ | |
0, /* tp_getset */ | |
0, /* tp_base */ | |
0, /* tp_dict */ | |
0, /* tp_descr_get */ | |
0, /* tp_descr_set */ | |
offsetof(PyModuleObject, md_dict), /* tp_dictoffset */ | |
module___init__, /* tp_init */ | |
PyType_GenericAlloc, /* tp_alloc */ | |
PyType_GenericNew, /* tp_new */ | |
PyObject_GC_Del, /* tp_free */ | |
}; |
我们可以看到,moduleobject 实现了自己的 tp_getattro 方法,我们来看看是如何获取属性的
static PyObject* | |
module_getattro(PyModuleObject *m, PyObject *name) | |
{ | |
PyObject *attr, *mod_name, *getattr; | |
/** | |
首先调用 PyObject_GenericGetAttr 从 moduleobject 中获取指定属性,PyObject_GenericGetAttr 具体是怎么实现的可以 | |
我往期的 getattr 实现原理剖析,有详细讲解到描述器的作用 | |
**/ | |
attr = PyObject_GenericGetAttr((PyObject *)m, name); | |
if (attr || !PyErr_ExceptionMatches(PyExc_AttributeError)) { | |
// 当找到属性并且没有匹配到异常 (AttributeError),则直接返回 | |
return attr; | |
} | |
PyErr_Clear(); | |
// 如果没有找到属性,则走到这 | |
if (m->md_dict) { | |
// 获取 moduleobject 的__dict__,并从其中获取__getattr__方法 | |
_Py_IDENTIFIER(__getattr__); | |
getattr = _PyDict_GetItemId(m->md_dict, &PyId___getattr__); | |
if (getattr) { | |
// 如果定义了__getattr__方法,则调用__getattr__方法并返回结果 | |
return PyObject_CallOneArg(getattr, name); | |
} | |
// 如果没有找到__getattr__方法,则从__dict__中获取__name__属性 | |
mod_name = _PyDict_GetItemId(m->md_dict, &PyId___name__); | |
if (mod_name && PyUnicode_Check(mod_name)) { | |
Py_INCREF(mod_name); | |
//__spec__这个属性我并不了解,因为几乎接触不到,哭~~~ | |
//__spec__用于存储有关模块的信息,以及在导入过程中指定如何加载和初始化模块的信息。该属性在 3.3 引入的,作为对导入机制的增强。 | |
// 具体细节自行了解 | |
PyObject *spec = _PyDict_GetItemId(m->md_dict, &PyId___spec__); | |
Py_XINCREF(spec); | |
// 下面都是抛异常处理,不是重点 | |
if (_PyModuleSpec_IsInitializing(spec)) { | |
PyErr_Format(PyExc_AttributeError, | |
"partially initialized " | |
"module '%U' has no attribute '%U' " | |
"(most likely due to a circular import)", | |
mod_name, name); | |
} | |
else { | |
PyErr_Format(PyExc_AttributeError, | |
"module '%U' has no attribute '%U'", | |
mod_name, name); | |
} | |
Py_XDECREF(spec); | |
Py_DECREF(mod_name); | |
return NULL; | |
} | |
} | |
PyErr_Format(PyExc_AttributeError, | |
"module has no attribute '%U'", name); | |
return NULL; | |
} |
剩下的字节码(STORE_NAME、LOAD_CONST、RETURN_VALUE)都是一样的步骤,存储变量,return None,到这里分析的就差不多了,如果还有其它细节或者错误,可以留下您的评论作以修正!