已经隔了好久没有更新文章了🙄🙄,今天我们来学习一下 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,到这里分析的就差不多了,如果还有其它细节或者错误,可以留下您的评论作以修正!

Edited on Views times

Give me a cup of [coffee]~( ̄▽ ̄)~*

小芳芳 WeChat Pay

WeChat Pay

小芳芳 Alipay

Alipay