《python解释器源码剖析》第15章--python模块的动态加载机制

15.0 序

在之前的章节中,我们考察的东西都是局限在一个模块(在python中就是module)内。然而现实中,程序不可能只有一个模块,更多情况下一个程序会有多个模块,而模块之间存在着引用和交互,这些引用和交互也是程序的一个重要的组成部分。本章剖析的就是在python中,一个模块是如何加载、并引用另一个模块的功能的。对于一个模块,肯定要先从硬盘加载到内存。

15.1 import前奏曲

我们以一个简单的import为序幕

# a.py
import sys 
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sys)
              6 STORE_NAME               0 (sys)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE

我们发现对应的字节码真的是简单无比。先不管开头的两个LOAD_CONST,我们看到了IMPORT_NAME,这个可以类比LOAD_NAME将sys这个module加载进来,然后调用STORE_NAME存储在当前PyFrameObject的local空间中,然后当我们调用sys.path的时候,虚拟机就能很轻松地找到sys这个符号对应的值了。因此本质上和创建一个变量是没有什么区别的,关键就是这个IMPORT_NAME,我们看看它的实现,知道从哪里看吗?我们说python中所有的指令集的实现都在ceval.c的那个大大的for循环中的大大的switch中。

        TARGET(IMPORT_NAME) {
            //PyUnicodeObject对象,比如import pandas,那么这个name就是字符串pandas
            PyObject *name = GETITEM(names, oparg);
            //我们看到这里的有一个fromlist和level,得到的结果是None和0
            //我们再看一下刚才的字节码,我们发现在IMPORT_NAME之前有两个LOAD_CONST,将0和None压入了运行时栈
            //显然这里是将运行时栈中的内容弹出来,分别赋值给fromlist和level
            PyObject *fromlist = POP(); //None
            PyObject *level = TOP(); // 0
            PyObject *res;
            // 调用了import_name,然后返回res
            res = import_name(f, name, fromlist, level);
            Py_DECREF(level);
            Py_DECREF(fromlist);
            //将res压入运行时栈
            SET_TOP(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }

因此重点在import_name这个函数中,但是在此之前我们需要重点关注一下这个fromlist和level,而这一点我们从python的层面来介绍。我们知道在python中,我们导入一个模块直接通过import?sys即可, 但是除了import,我们还可以使用__import__,这个__import__是解释器使用的一个函数,不推荐我们直接使用,但是我想说的是import os等价于os =?__import__("os")

os = __import__("os")
print(os)  # <module 'os' from 'C:\\python37\\lib\\os.py'>

# 当然我们使用__import__的时候,还可以使用其他的名字
aaa = __import__("sys")
print(aaa.prefix)  # C:\python37

但是问题来了

m = __import__("os.path")
print(m)  # <module 'os' from 'C:\\python37\\lib\\os.py'>

# 我们惊奇地发现,居然还是os模块,按理说应该是os.path(windows系统对应ntpath)啊
m1 = __import__("os.path", fromlist=[""])
print(m1)  # <module 'ntpath' from 'C:\\python37\\lib\\ntpath.py'>

# 你看到了什么,fromlist,没错,我们加上一个fromlist,就能导入子模块了

为什么会这样呢?我们来看看__import__这个函数的解释,这个是pycharm给抽象出来的。

def __import__(name, globals=None, locals=None, fromlist=(), level=0):
    """
    __import__(name, globals=None, locals=None, fromlist=(), level=0) -> module

    Import a module. Because this function is meant for use by the Python
    interpreter and not for general use, it is better to use
    importlib.import_module() to programmatically import a module.

    The globals argument is only used to determine the context;
    they are not modified.  The locals argument is unused.  The fromlist
    should be a list of names to emulate ``from name import ...'', or an
    empty list to emulate ``import name''.
    When importing a module from a package, note that __import__('A.B', ...)
    returns package A when fromlist is empty, but its submodule B when
    fromlist is not empty.  The level argument is used to determine whether to
    perform absolute or relative imports: 0 is absolute, while a positive number
    is the number of parent directories to search relative to the current module.
    """
    pass

大意就是,此函数会由import语句调用,当我们import一个模块的时候,解释器底层就会调用__import__import?os表示将"os"这个字符串传入__import__中,导入os模块,并将返回值再次赋值给符号os,也就是os?= __import__("os")。我们是可以通过这种方式来导入模块,但是python不建议我们这么做。而globals参数则是确定import语句包的上下文,一般直接传globals()即可,但是locals参数我们不会用,但是一般情况下globals和locals我们都不用管。

fromlist我们刚才已经说了,__import__("os.path"),如果是这种情况的话,那么导入的不是os.path,还是os这个外层模块,如果想导入os.path,那么只需要给fromlist传入一个非空列表即可,其实不仅仅是非空列表,只要是一个非空的可迭代对象就行。而level如果是0,那么表示仅执行绝对导入,如果是一个正整数,表示要搜索的父目录的数量。一般这个值也不需要传递。

这个方法有什么作用,就是如果我们有一个字符串a,其值为pandas,我想导入这个模块,该怎么做呢?显然就可以使用这种方式,但是这种方式导入的话,python官方不推荐使用__import__,而是希望我们使用一个叫做importlib的模块

import importlib

a = "pandas"
pd = importlib.import_module(a)
# 通过这种方式依旧是可以导入的
print(pd)  # <module 'pandas' from 'C:\\python37\\lib\\site-packages\\pandas\\__init__.py'>

# 另外
m = importlib.import_module("os.path")
# 我们看到这种模式是支持导入包里面的包、或者模块的
print(m)  # <module 'ntpath' from 'C:\\python37\\lib\\ntpath.py'>

扯了这么多,我们来看看之前源码中说的import_name

//ceval.c
// 这个函数接收了四个参数,f:栈帧,name:模块名,fromlist:一个None,level:0
res = import_name(f, name, fromlist, level);

static PyObject *
import_name(PyFrameObject *f, PyObject *name, PyObject *fromlist, PyObject *level)
{
    _Py_IDENTIFIER(__import__);
    PyObject *import_func, *res;
    PyObject* stack[5];

    //获取内建函数__import__,但是此时的__import__已经不是一个PyFunctionObject了
    //而是一个PyCFunctionObject,我们上一章分析python初始化动作时,我们看到在初始化
    //__builtin__module时,函数已经摇身一变,被包装成PyCFunctionObject了
    import_func = _PyDict_GetItemId(f->f_builtins, &PyId___import__);
    //为NULL获取失败
    if (import_func == NULL) {
        PyErr_SetString(PyExc_ImportError, "__import__ not found");
        return NULL;
    }

    /* Fast path for not overloaded __import__. */
    //判断import是否被重载了
    if (import_func == PyThreadState_GET()->interp->import_func) {
        int ilevel = _PyLong_AsInt(level);
        if (ilevel == -1 && PyErr_Occurred()) {
            return NULL;
        }
        //未重载的话,调用PyImport_ImportModuleLevelObject
        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;
}

//import.c
PyObject *
PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
                                 PyObject *locals, PyObject *fromlist,
                                 int level)
{
    _Py_IDENTIFIER(_handle_fromlist);
    PyObject *abs_name = NULL;
    PyObject *final_mod = NULL;
    PyObject *mod = NULL;
    PyObject *package = NULL;
    PyInterpreterState *interp = PyThreadState_GET()->interp;
    int has_from;

    //名字不可以为空
    if (name == NULL) {
        PyErr_SetString(PyExc_ValueError, "Empty module name");
        goto error;
    }

    //名字必须是PyUnicodeObject
    if (!PyUnicode_Check(name)) {
        PyErr_SetString(PyExc_TypeError, "module name must be a string");
        goto error;
    }
    //level不可以小于0
    if (level < 0) {
        PyErr_SetString(PyExc_ValueError, "level must be >= 0");
        goto error;
    }

    //level大于0
    if (level > 0) {
        //在相应的父目录寻找,得到abs_name
        abs_name = resolve_name(name, globals, level);
        if (abs_name == NULL)
            goto error;
    }
    //否则的话,说明level==0,因为level要求是一个大于等于0的整数
    else {  /* level == 0 */
        if (PyUnicode_GET_LENGTH(name) == 0) {
            PyErr_SetString(PyExc_ValueError, "Empty module name");
            goto error;
        }
        //此时直接将name赋值给abs_name
        //因为此时是绝对导入
        abs_name = name;
        Py_INCREF(abs_name);
    }

    //调用PyImport_GetModule获取模块
    //注意:这个模块会从sys.modules里面获取,并不是直接导入模块
    //我们说在python中,一个模块不会被重复导入,一旦导入之后会加入到sys.modules中
    //当导入的时候,会从sys.modules里面查找,比如有一个a.py,里面写了一个print
    //但是我们导入两次,但是这个print只会打印一次,因此模块只会导入一次
    mod = PyImport_GetModule(abs_name);
    if (mod == NULL && PyErr_Occurred()) {
        goto error;
    }
    ...
    ...
    else {
        //调用函数,导入模块
        final_mod = _PyObject_CallMethodIdObjArgs(interp->importlib,
                                                  &PyId__handle_fromlist, mod,
                                                  fromlist, interp->import_func,
                                                  NULL);
    }

  error:
    Py_XDECREF(abs_name);
    Py_XDECREF(mod);
    Py_XDECREF(package);
    if (final_mod == NULL)
        remove_importlib_frames();
    return final_mod;
}

我们知道在python中,从语法层面上来讲有很多种写法,比如:

import a
import a.b
from a import b
from a import b as bb
from a import *

从import的目标来说,可以有系统的标准模块,还有用户自己写的模块,而用户写的模块又分为python原生实现的模块和C语言实现并以dll或者so形式存在的模块,下面我们将一一介绍。

15.2 python中import机制的黑盒探测

同golang的package、c++的namespace,python通过module机制和之后会挖掘的package机制来实现对系统复杂度的分解,以及保护命名空间不受污染。这里的module就是一个python可以导入的文件,而package是一个python可以导入的包、或者说是目录。所以,package可以包含module

通过module和package,我们可以将某个功能、某种抽象进行独立的实现和维护,在module和package的基础之上构建软件,这样不仅使得软件的架构清晰,而且也能很好的实现代码复用。

15.2.1 标准import

15.2.1.1 python内建module

sys这个模块恐怕是使用的非常频繁的module了,我们就从这位老铁入手。在对python运行环境的初始化分析中,我们看到了dir,这个小工具是我们探测import的杀手锏。如果你在交互式环境下输入dir(),那么会打印当前local命名空间的所有符号,如果有参数,则将参数视为对象,输出该对象的所有属性。我们先来看看import动作对当前命名空间的影响

我们看到当我们进行了import动作之后,当前的local命名空间增加了一个sys符号。而且通过type操作,我们看到这个sys符号对应这一个module对象,当然在cpython中是一个PyModuleObject。当然虽然写着<class ‘module‘>,但是我们在python中是看不到的。但是它是一个class,那么就一定继承object,并且元类为type

这与我们的分析是一致的。言归正传,我们看到import机制影响了当前local命名空间,使得加载的module对象在local空间成为可见的。实际上,这和我们创建一个变量的时候,也会影响local命名空间。引用该module的方法正是通过module的名字,即这里的sys。

不过这里还有一个问题,我们来看一下。

惊了,我们输入sys,居然是builtin,是内置的。可既然如此,那为什么我们不能直接使用,还需要导入呢?其实不光是sys,在python初始化的时候,就已经将一大批的module的加载到了内存中。但是为了使得local命名空间能够达到最干净的效果,python并没有讲这些符号暴露在local命名空间中,而是需要用户显式的使用import机制来将这个符号引入到local命名空间中,以便程序能够使用这个符号背后的对象。

我们知道,凡是加载进内存的模块都保存在sys.modules里面,尽管local里面没有,但是sys.modules里面是跑不掉的。

import sys
# 这个modules是一个字典,里面分别为module name和PyModuleObject
# 里面有很多模块,我就不打印了,另外感到意味的是,居然把numpy也加载进来了
modules = sys.modules

np = modules["numpy"]
arr = np.array([1, 2, 3, 4, 5])
print(np.sum(arr))  # 15

os_ = modules["os"]
import os
print(id(os) == id(os_))  # True

一开始这些模块名是不在local里面的,除非我们显式导入,但是即便我们导入,这些模块也不会被二次导入,因为已经在初始化的时候就被加载到内存里面了。因此对于已经在sys.modules里面的模块来说,导入的时候只是加到local空间里面去了,所以代码中的os和os_的id是一样的。如果我们在python启动之后,导入一个sys.modules中不存在的模块,那么会同时进入local和sys.modules

15.2.1.2 用户自定义module

我们知道,对于那些内建模块(解释器初始化的时候自动加入到sys.modules的模块),如果import,只是将该模块暴露在了local命名空间中。下面我们看看对于那些没有在初始化的时候加载到内存的module进行import的时候,会出现什么样动作。当然正如我们之前说的,用户可以通过py文件创建自己的module、python标准库中的.py module、第三方.py module,当然这些都是py文件,以及通过C语言创建dll、so,生成python的扩展module,这些都不是python的内建module。不过我们目前不区分py文件和dll文件,只以py文件作为例子,探探路。

# a.py
a = 1
b = 2
import sys
print("a" in sys.modules)  # False
import a
print("a" in sys.modules)  # True
print(dir())
# ['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'sys']

print(id(a))  # 2653299804976
print(id(sys.modules["a"]))  # 2653299804976

print(type(a))  # <class 'module'>

操作type()的结果显示,import机制确实创建了一个新的module。而且也确实如我们之前所说python对a这个module,不仅将其引入进当前的local命名空间中,而且这个被动态加载的module也在sys.module中拥有了一席之地。并且local中符号a和sys.modules中符号a背后隐藏的是同一个PyModuleObject对象。然后我们再来看看这个module对象。

import a
print(dir(a))  # ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b']
print(a.__dict__.keys())
# dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', 'a', 'b'])

print(a.__name__)  # a
print(a.__file__)  # C:\Users\satori\Desktop\love_minami\a.py

这里可以看到,module对象内部实际上是通过一个dict在维护所有的{属性: 属性值},里面有module的元信息(名字、文件路径)、以及module里面的内容。对,说白了,同class一样,module又是一个命名空间

另外如果此时你查看a.py所在目录的__pycache__目录,你会发现里面有一个a.pyc,说明python在导入的时候先生成了pyc,然后导入了pyc。并且我们通过dir(a)查看的时候,发现里面有一个__builtins__符号,那么这个__builtins__和我们之前说的那个__builtins__是一样的吗?

import a

# 我们之前说获取builtins可以通过import builtins的方式导入,但其实也可以通过__builtins__获取
print(id(__builtins__), type(__builtins__))  # 1745602347792 <class 'module'>
print(id(a.__dict__["__builtins__"]), type(a.__dict__["__builtins__"]))  # 1745602345408 <class 'dict'>

尽管它们都叫__builtins__,但一个是module,一个是dict。我们通过__builtins__直接获取的是一个module,里面存放了int、str、globals等内建对象和内建函数等等,我们直接输入int、str、globals和通过—__builtins__.int__builtins__.str__builtins__.globals的效果是一样的,我们输入__builtins__可以拿到这个内置模块,通过这个内置模块去获取里面的内容,当然也可以直接获取里面的内容,因为这些已经是全局的了。

但是a.__dict__["__builtins__"]是一个dict,这就说明两个从性质上就是不同的东西,但即便如此,就真的一点关系也没有吗?

import a

print(id(__builtins__.__dict__))  # 2791398177216
print(id(a.__dict__["__builtins__"]))  # 2791398177216

我们看到还是有一点关系的,和类、类的实例对象一样,每一个模块也有自己的属性字典__dict__,记录了自身的元信息、里面存放的内容等等,对于a.__dict__["__builtins__"]来说,拿到的就是__builtins__.__dict__,所以说__builtins__是一个模块,但是这个模块有一个__dict__属性字典,而这个字典是可以通过module.__dict__["__builtins__"]来获取的,因为任何一个模块都可以使用__builtins__里面的内容,并且所有模块对应的__builtins__都是一样的。所以当你直接打印a.__dict__的时候会输出一大堆内容,因为输出的内容里面不仅有当前模块的内容,还有__builtins__.__dict__。再提一遍属性字典,当我们通过obj.attr的时候,本质上是通过obj.__dict__["attr"]获取的。

import a

print(a.__dict__["__builtins__"]["list"]("abcd"))  # ['a', 'b', 'c', 'd']

# a.__dict__["__builtins__"]就是__builtins__.__dict__这个属性字典
# __builtins__.list就等价于__builtins__.__dict__["list"]
# 说白了,就是我们直接输入的list

print(a.__dict__["__builtins__"]["list"] is list)  # True

# 回顾之前的内容

# 我们说,模块名是在模块的属性字典里面
print(a.__dict__["__name__"] == a.__name__ == "a")  # True

# __builtins__里面的__name__就是builtins
print(__builtins__.__dict__["__name__"])  # builtins

# 还记得如何获取当前文件的文件名吗
print(__name__)   # __main__
# 咦,可能有人说,这不是从__builtins__里面获取的吗?
# 我们之前说了,__name__已经被设置到local命名空间了
# 所以这个__name__是从local里面拿的,尽管我们没有设置,但是它确确实实在里面
# 而且local里面有的话,就不会再去找__builtins__

15.2.2 嵌套import

我们下面来看一下import的嵌套,所谓import的嵌套就是指,假设我import a,但是在a中又import b,我们来看看这个时候会发生什么有趣的动作。

# a.py
import sys
# b.py
import a
"""
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (a)
              6 STORE_NAME               0 (a)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE
"""

在a.py中导入sys,在另一个模块导入a,打印字节码指令,我们只看到了IMPORT_NAME?0 (a),似乎并没有看到a模块中的动作。我们说了,使用dis模块查看的字节码是分层级的,只能看到import a这个动作,a里面做了什么是看不到的。

# b.py
import a
import sys

print(a.sys is sys is sys.modules["sys"] is a.__dict__["sys"])  # True

首先我们import a,那么a模块就在当前模块的属性字典里面了,我通过a这个符号是可以直接拿到其对应的模块的。但是在a中我们又import?sys,那么这个sys模块就已经在a模块对应的属性字典里面了,也就是说,我们在b.py中通过a.sys是可以直接拿到sys模块的。但是,我们第二次导入sys的时候,会怎么样呢?首先我们在a中已经导入了sys,那么sys这个模块就已经在sys.modules里面了。那么当我们再导入sys的时候,就直接从sys.modules里面去找了,因此不会二次导入。为了更直观的验证,我们再举一个例子:

# a.py
print(123)

# b.py
import a

# c.py
import a

以上是三个文件,我们下面再创建一个文件来导入一下:

import a
# 导入模块就相当于把该模块的内容拿出来,在当前模块执行一遍
# 并且里面的内容是在被导入模块的命名空间里面的,比如这里只能通过符号a来获取a里面的内容
"""
123
"""
import b

"""
123
"""
import a
import b
import c

"""
123
"""

你看到了什么,我们导入a的时候,相当于把a里面内容取出来执行一遍,打印了123。导入b的时候,把b的内容拿到当前模块里面执行,b里面又导入了a,然后又把a里面的内容取出来拿到b里面执行,依旧打印123。同理导入c也是一样的,但是我们同时导入a、b、c三个模块,却只打印了一遍123,明明应该打印3次的啊。还是我们之前说的,当我在import a的时候,a这个模块就已经在sys.modules里面了,那么当我import b、b里面 import a的时候,直接去sys.modules把a这个符号对应的模块取出来加到模块b的local命名空间即可,不会再进行导入了,同理c也是一样。

所以我们发现sys.modules是一个全局的模块空间,不管在执行程序涉及到几个py文件,各个py文件导入了几个模块,只要导入就会先到sys.modules里面找,找不到再导入模块并加入到sys.modules里面,只要sys.modules里面有,就直接取并加入到local命名空间、不会二次导入。所以每一个module的import不会影响其它的module,只会影响自身module的命名空间,或者module自身维护的那个dict对象(__dict__),local、global空间的内容在__dict__中都能找到

所以我们可以把sys.modules看成是一个大仓库,任何导入了的模块都在这里面。如果再导入的话,在sys.modules里面找到了,就直接返回即可,这样可以避免重复import

15.2.3 import package

package就是所谓的包(大白话就是一个文件夹,雾),我们写的多个逻辑或者功能上相关的函数、类可以放在一个module里面,那么多个module是不是也可以组成一个package呢?如果说module是管理class、函数、一些变量的机制,那么package就是管理module的机制,当然啦,多个小的package又可以聚合成一个较大的package。

因此在python中,module是由一个单独的文件来实现的,可以使py文件、或者pyc文件、pyd文件、甚至是用C扩展的dll文件。而对于package来说,则是一个目录,里面容纳了py、pyc或者dll文件,这种方式就是把多个module聚合成一个package的具体实现。

现在我有一个名为package的模块,里面有一个a.py

a.py内容如下

a = 123
b = 456
print(">>>")

现在我们来导入它

import module
print(module)  # <module 'module' (namespace)>

在python2中,这样是没办法导入的,因为如果一个目录要成为python中的package,那么里面必须要有一个__init__文件,但是在python3中没有此要求。而且我们发现print之后,显示这package也是一个module对象,因此python对于module和package的底层定义其实是很灵活的,并没有那么僵硬。

import module
print(module.a)  # AttributeError: module 'module' has no attribute 'a'

然而此时神奇的地方出现了,我们调用module.a的时候,告诉我们没有a这个属性。很奇怪,我们的module里面不是有a.py吗?首先python导入一个包,会先执行这个包的__init__文件,只有在__init___文件中导入了,我们才可以通过包名来调用。如果这个包里面没有__init__文件,那么你导入这个包,是什么属性也用不了的。光说可能比较难理解,我们来演示一下。我们先来创建__init__文件,但是里面什么也不写。

import module
print(module)
# <module 'module' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\__init__.py'>

此时我们又看到了神奇的地方,我们在module目录里面创建了__init__文件之后,再打印module,得到结果就变了,告诉我们这个包来自于该包里面的__init__文件,所以就像我们之前说的,python对于包和模块的概念区分的不是很明显,我们把包就当做该包下面的__init__文件即可,这个__init__中定义了什么,那么这个包里面就有什么。

# module/__init__.py
import sys
from . import a
name = "satori"

from?.?import?a这句话表示导入module这个下面的a.py,但是直接像import sys那样import a不行吗?答案是不行的,至于为什么我们后面说。我们在__init__.py中导入了sys模块、a模块,定义了name属性,那么就等于将sys、a、name加入到了module这个包的属性字典里面去了。因为我们说过,对于python中的包,那么其等价于里面的__init__文件,这个文件有什么,那么这个包就有什么。既然我们在__init__.py中导入了sys、a模块,定义了name,那么这个文件的属性字典里面、或者也可以说local空间里面就有了"sys":?sys,?"a":?a,?"name":?"satori"这三个entry,而我们又说了__init__.py里面有什么,那么通过包名就能够调用什么。

import module

print(module.a)
print(module.a.a)
print(module.sys)
print(module.name)
"""
>>>
<module 'module.a' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\a.py'>
123
<module 'sys' (built-in)>
satori
"""
# 首先在a里面有一个print(123)
# 而我们说导入一个模块,就相当于把这个模块拿过来执行一遍。导入一个包则是把这个包里面的`__init__`执行一遍
# 那么在__init__里面导入a的时候,就会打印这个print

# 另外此时如果我再单独导入module里面的a模块的话,会怎么样呢?
# 下面这两种导入方式后面会介绍
import module.a
from module import a
# 我们看到a里面的print没有被打印,证明确实模块、包不管以怎样的方式被导入,只要被导入了,那么就只会被导入一遍

所以这个__init__.py的作用我们就很清晰了,当我们只想导入一个包的时候,那个通过这包能够使用哪些属性,就通过__init__.py中定义,只有在__init__中定义了,才可以通过包名直接调用。

15.2.3.1 相对导入与绝对导入

我们刚才使用了一个from .?import?a的方式,这个.表示当前文件所在的目录,这行代码就表示,我要导入a这个模块,不是从别的地方导入,而是从该文件所在的目录里面导入。如果是..就表示该目录的上一层目录,三个.、四个.依次类推。我们知道a模块里面还有一个a这个变量,那如果我想在__init__.py中导入这个变量该怎么办呢?直接from?.a?import?a即可,表示导入当前目录里面的a模块里面的a变量。如果我们导入的时候没有.的话,那么表示绝对导入,python虚拟机就会按照sys.path定义的路径去找。假设我们在__init__.py当中写的是不是from . import?a,而是import?a,那么会发生什么后果呢?

import module
"""
  File "C:\Users\satori\Desktop\love_minami\module\__init__.py", line 2, in <module>
    import a
ModuleNotFoundError: No module named 'a'
"""

我们发现报错了,告诉我们没有a这个模块,可是我们明明在module包里面定义了呀。还记得之前说的导入一个模块、导入一个包会做哪些事情吗?导入一个模块,会将该模块里面拿过来执行一遍,导入包会将该包里面的__init__.py文件拿过来执行一遍。注意:我们把拿过来三个字加粗了。

我们注意到,这个test.py里面导入了module,那么就相当于把module目录里面__init__.py拿到test里面来执行一遍,然后它们具有单独的空间,是被隔离的,调用需要使用符号module来调用。但是正如我们之前所说,是拿过来执行,所以这个__init__.py里面的内容是拿过来、然后在test(在哪里导入的就是哪里)里面执行的。所以由于import a这行代码表示绝对导入,就相当于在test模块里面导入,会从sys.path里面搜索,但是a是在module包里面,那么此时还能找到这个a吗?显然是不能的。那from?.?import?a为什么就好使呢?因为这种导入表示相对导入,就表示要在__init__.py所在目录里面找,那么不管在什么地方导入这个包,由于这个__init__.py的位置是不变的,所以from?.?import?a这种相对导入的方式总能找到对应的a。至于sys(标准库、第三方包),因为它是在sys.path里面的,在哪儿都能找得到,所以可以绝对导入,貌似也只能绝对导入。并且我们知道每一个模块都有一个__file__属性,当然包也是。如果你在一个模块里面print(__file__),那么不管你在哪里导入这个模块,打印的永远是这个模块的地址。

另外关于相对导入,一个很重要的一点,一旦一个模块出现了相对导入,那么这个模块就不能被执行了,它只可以被导入。

import sys
from . import a
name = "satori"
"""
    from . import a
ImportError: attempted relative import with no known parent package
"""

此时如果我试图执行__init__.py,那么就会给我报出这个错误。另外即便导入一个内部具有"相对导入"的模块,那么此模块和导入的模块也不能在同一个包内,我们要执行的此模块至少要在导入模块的上一级,否则执行此模块也会报出这种错误。为什么会有这种情况,很简单。想想为什么会有相对导入,就是希望这些模块在被其它地方导入的时候能够准确记住要导入的包的位置。那么这些模块肯定要在一个共同的包里面,然后我们在包外面使用。所以我们导入一个具有相对导入的模块时候,那么我们当前模块和要导入的模块绝对不能在同一个包里面。

就像我们这样,test.py和module包是分开的,module包里面出现了相对导入,那么它应该作为一个整体被外面的人使用,我们使用的test.py和module是同级的,那么它就比module里面的模块高一级。并且注意的是,module包里面的相对导入不要超出module包这个范围。我们知道.表示当前文件所在的目录,那么n个.就表示当前的目录的上n-1级目录。既然是在module这个包里面,那么范围就不要越过module这个包。

# module/__init__.py
import sys
from .. import test
name = "satori"

显然.表示module目录,那么..就是module所在的目录了,它越过module目录,在里面导入了test,这说明什么?这意味着module虽然还是一个包,但它只是一个小包,module所在目录的包才是最终的包

也就说此时,love_minami这个目录才是一个外层的包,module包不过是这里面的小包。我们说一个package里面是可以嵌套package的。

# 1.py
import module
"""
    from .. import test
ValueError: attempted relative import beyond top-level package
"""

如果我此时再导入module,就会报错,这里提示我们相对导入越过了顶层的package。我们说过进行相对导入的模块只能被其他模块导入,不可以执行。另外,我们要执行的模块一定要在出现了相对导入的模块的上一级,准确的说相对导入里面的最高层级的目录的上一层。比如__init__里面出现了..,表示love_minami这个目录,那么你要想导入module,那么你执行的模块至少要在..的上一级,也就是love_minami所在的目录,因为相对导入超出了module包,那么此时love_minami就应该作为一个package,如果想导入,那么你至少要和love_minami同级。关于相对导入,解释起来比较费劲,能大致理解就好,总之希望记住以下几点:

  • 出现了相对导入,那么这个模块是不能被执行的,只能被导入
  • 出现了相对导入的模块,那么它至少要在一个包内,相对导入不能越过这个包。
  • 执行导入的模块必须至少和这个导入的模块所在的包是同级的。

关于相对导入,解释起来实在不好说,有点语无伦次,或者说的不严谨。不过可以自己尝试一下,还是很容易理解的。

15.2.3.2 import的另一种方式

我们要导入module包里面的a模块,除了可以import module(_init__.py里面导入了a),还可以通过import?module.a的方式,另外如果是这种导入方式,那么module里面可以没有__init__.py文件,因为我们导入module包的时候,是通过module来获取a,所以必须要有__init__.py文件、并且里面导入a。但是在导入module.a的时候,就是找module.a,所以此时是可以没有__init__.py文件的

# module/__init__.py
name = "satori"

此时module包里面的__init__.py只有name这个变量,下面我们来通过module.a的形式导入。

import module.a

print(module.a.a)
"""
>>>
123
"""

# 当import module.a的时候,会执行里面的print
# 然后可以通过module.a获取a里面的属性,这很好理解

# 但是,没错,我要说但是了
print(module.name)  # satori

惊了,我们在导入module.a的时候,也把module导入进来了,为了更直观的看到现象,我们在__init__.py里面打印一句话

# module/__init__.py
name = "satori"
print("我是module下面的__init__")
import module.a
"""
我是module下面的__init__
>>>
"""

所以一个有趣的现象就产生了,我们是导入module.a,但是把module也导入进来了。而且通过打印的顺序,我们看到是先导入了module,然后在导入module下面的a。如果在__init__.py里面导入了a,那么import?module.a就只会导入module,module.a就不会二次导入了

# # module/__init__.py
name = "satori"
print("我是module下面的__init__")
from . import a
import module.a
"""
我是module下面的__init__
>>>
"""
# 我们看到>>>只被打印了一次,证明没有进行二次导入

所以通过module.a的方式来导入,即使没有__init__.py文件依旧是可以访问的,因为这是我在import的时候指定的。我们可以看一下sys.modules

import module.a
import sys

print(sys.modules["module"])
# <module 'module' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\__init__.py'>
print(sys.modules["module.a"])
# <module 'module.a' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\a.py'>

我们看到里面有一个模块就叫做module.a,所以为什么通过import module.a的方式导入不需要__init__.py就很清晰了,因为我们并不是通过module来找a这个模块,而是把a这个模块我们通过import module.a的方式导入进来之后,就叫做module.a。但是为什么叫做module.a而不是a呢?因为如果还有一个包里面也有一个a的话,不就冲突了吗?导入module.a和module1.a如果都叫做a的话,那么它们如何才能在sys.modules里面和平共存呢?另外,我们这里的包名叫module,是不是不太合适啊,应该叫做package更好一些,不过无所谓啦。

不过这里还有一个问题就是刚才说的,我们在导入module.a的时候,会先加载module,然后才会导入module.a,并且导入module.a的时候,还可以单独使用module。毕竟这不是我们期望的结果,因为导入module.a的话,那么我们只是想使用module.a,不打算使用module,python为什么要这么做呢?事实上,这对python而言是必须的,根据我们对python虚拟机的执行原理的了解,python要想执行module.a,那么肯定要先从local空间找到module,然后才能找到a,如果不找到module的话,那么对a的查找也就无从谈起。可我们刚才不是说module.a是一个整体吗?事实上尽管是一个整体,但并不是说有一个模块,这个模块就叫做module.a。准确的说import module.a表示先导入module,然后再将module下面的a加入到module的属性字典里面。我们说当module这个包里面没有__init__.py的时候,那个这个包是无法使用的,因为属性字典里面没有相关属性,但是当我们import module.a的时候,python会先导入module这个包,然后自动帮我们把a这个模块加入到module这个包的属性字典里面。

import module.a

# 此时的__init__里面啥也没有
# 我们把__builtins__给pop掉,不然会输出一大堆东西
module.__dict__.pop("__builtins__")
for name, m in module.__dict__.items():
    print(name, m)
"""
__name__ module
__doc__ None
__package__ module
...
...
...
a <module 'module.a' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\a.py'>
"""

我们看到__package__就是包名,如果是模块那么就是None,然后我们看到了a。因此这种方式就跟我们定义了__init__文件、并在里面导入了a是一样的。

假设module这个包里面有a和b两个py文件,那么我们执行import?module.aimport?module.b会进行什么样的动作应该就了如指掌了吧。执行import?module.a,那么会先导入module,然后把a加到module的属性字典里面,执行import?module.b,还是会先导入包module,但是包module在上一步已经被导入了,所以此时直接会从sys.modules里面获取,然后再把b加入到module的属性字典里面。所以如果__init__.py里面有一个print的话,那么两次导入显然只会print一次,这种现象是由python对包中的模块的动态加载机制决定的。还是那句话,一个包你就看成是里面的__init__.py文件即可,python对于包和模块的区分不是特别明显。

import module

# 有__init__文件
print(module.__file__)  # C:\Users\satori\Desktop\love_minami\module\__init__.py
print(module)  # <module 'module' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\__init__.py'>
import module

# 没有__init__文件
print(module.__file__)  # None
print(module)  # <module 'module' (namespace)>

我们看到如果包里面有__init__.py文件,那么这个包的__file__属性就是其内部的__init__.py文件,打印这个包,显示的也是其内部的__init__.py模块。如果没有__init__.py文件,那么这个包的__file__就是一个None,打印这个包,显示其是一个空间。另外,我们知道任何一个模块(即使里面什么也不写)的属性字典里面都是有__builtins__属性的,因为可以直接使用内置的对象、函数等等。而__init__.py也是属于一个模块,所以它也有__builtins__属性的,由于一个包指向了内部的__init__.py,所以这个包的属性字典也是有__builtins__属性的。但如果这个包没有__init__.py文件,那么这个包是没有__builtins__属性的。

import module

# 没有__init__.py文件
print(module.__dict__.get("__builtins__"))  # None
import module

# 有__init__.py文件
print(module.__dict__.get("__builtins__")["int"])  # <class 'int'>

15.2.3.3 路径搜索树

假设我有这样的一个目录结构

那么python会将这个结构进行分解,得到一个类似于树状的节点集合

然后从左到右依次去sys.modules中查找每一个符号所对应的module是否已经被加载,如果一个包被加载了,比如说包module(所以说这个名字起得实在不合适)被加载了,那么在包module对应的PyModuleObject中会维护一个元信息__path__,表示这个package的路径。比如我搜索A.a,当加载进来A的时候,那么a只会在A.__path__中进行,而不会在python的所有搜索路径中执行了。

import module

# 打印module的__path__
print(module.__path__)  # ['C:\\Users\\satori\\Desktop\\love_minami\\module']

# 导入sys模块
try:
    import module.sys
except ImportError as e:
    print(e)  # No module named 'module.sys'

# 显然这样是错的,因为导入module.sys,那么就将搜索范围只限定在module的__path__下面了

15.2.4 from与import

在python的import中,有一种精确控制所加载的对象的方法,通过from和import的结合,可以只将我们期望的module、甚至是module中的某个符号,动态地加载到内存中。这种机制使得python虚拟机在当前命名空间中引入的符号可以尽可能地少,从而更好地避免名字空间遭到污染。

按照我们之前所说,导入module下面的a模块,我们可以使用import?module.a的方式,但是此时a是在module的命名空间中,不是在我们当前模块的命名空间中。也就是说我们希望能直接通过符号a来调用,而不是module.a,此时通过from?...?import?...联手就能完美解决。

from module import a
"""
>>>
"""
print(dir())
# [... '__loader__', '__name__', '__package__', '__spec__', 'a']

import sys
print(sys.modules.get("module"))
print(sys.modules.get("module.a"))
print(sys.modules.get("a"))  # None

首先通过dir()我们看到,确确实实将a这个符号加载到当前的命名空间里面了,但是在sys.modules里面却没有a。还是之前说的,a这个模块是在module这个包里面的,你不可能不通过包就直接拿到包里面的模块,因此在sys.modules里面的形式其实还是module.a这样形式,只不过在当前模块的命名空间中是a,a被映射到sys.modules["module.a"],另外我们看到除了module.a,module也导入进来了,这个原因我们之前也说过了,不再赘述。所以我们发现即便我们是from ...?import?...,还是会触发整个包的导入。只不过我们导入谁(假设从a导入b),就把谁加入到了当前模块的命名空间里面(但是在sys.modules里面是没有b的,而是a.b),并映射到sys.modules["a.b"]。

所以我们见识到了,即便是我们通过from module import a,还是会导入module这个包的,只不过module这个包是在sys.modules里面,并没有暴露到local空间中,我们可以来证明这一点。

from module import a
"""
>>>
"""
import sys
print(id(sys.modules["module"]))  # 2015096408640
# 此时module尽管在sys.modules里面,但是却没有暴露在当前模块的local空间里面
# 那么此时导入module,显然会从sys.modules里面去找
import module
print(id(module))  # 2015096408640

完美证明我们之前的结论。

此外我们from?module import?a,导入的这个a是一个模块,但是模块a里面还有一个变量a,我们不加from,只通过import的话,那么最深也只能import到一个模块,不可能说直接import模块里面的某个变量、方法什么的。但是from?...?import?...的话,确是可以的,比如我们from?module.a?import?a,这句就表示我要导入module.a模块里面变量a

from module.a import a

print(locals())  # {..., ..., 'a': 123}
# 我们看到此时module.a里面的a就进入了local空间里面的

import sys
modules = sys.modules
print("a" in modules)  # False
print("module.a" in modules)  # True
print("module" in modules)  # True

我们导入的a是一个变量,并不是模块,所在sys.modules里面不会出现module.a.a这样的东西存在,但是这个a毕竟是从module.a里面导入的,所以module.a是会在sys.modules里面的,同理module.a表示从module的属性字典里面找a,所以module也是会进入sys.modules里面的。

最后还可以使用from module.a import *,这样的机制把一个模块里面所有的内容全部导入进来,本质和导入一个变量是一致的。但是在python中有一个特殊的机制,比如我们from p.m import *,如果m里面定义了__all__,那么只会导入__all__里面指定的属性。

# module/a.py
__all__ = ["a", "b"]
a = 123
b = 345
c = 456
print(">>>")

我们注意到在__all__里面只指定了a和b,那么后续通过from?module.a?import?*的时候,只会导入a和b,而不会导入c

from module.a import *
"""
>>>
"""
print("a" in locals() and "b" in locals())  # True
print("c" in locals())  # False

from module.a import c
print("c" in locals())  # True

我们注意到:通过import *导入的时候,是无法导入c的,因为c没有在__all__中。但是即便如此,我们也可以通过单独导入,把c导入进来。只是不推荐这么做,像pycharm这种智能编辑器也会提示:‘c‘ is not declared in __all__。因为既然没有在__all__里面,就证明这个变量是不希望被导入的,但是一般导入了也没关系。

15.2.5 符号重命名

我们导入的时候一般为了解决符号冲突,往往会起别名,或者说符号重命名。比如包a和包b下面都有一个模块叫做m,如果是from?a?import?mfrom?b?import?m的话,那么两者就冲突了,后面的m会把上面的m覆盖掉,不然python怎么知道要找哪一个m,所以这个时候我们会起别名,比如from?a?import m?as?m1from?b?import?m as?m2,不管是这种导入,import?a?as?xximport?a.a as?xx也是支持的,但是from?a?import?*是不支持as的。所以直接python都是将package、module或者变量自身的名字暴露给了local命名空间,而符号重命名则是python可以通过as关键字控制package、module、变量暴露给local命名空间的方式。

import module.a

print(module.a)
# <module 'module.a' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\a.py'>

print(module)
# <module 'module' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\__init__.py'>
import module.a as  A

print(A)
# <module 'module.a' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\a.py'>

import sys
print("module.a" in sys.modules)  # True
print("module" in sys.modules)  # True
print(module)
# NameError: name 'module' is not defined

看到结论我相信就应该心里有数了,不管我们有没有as,既然import?module.a,那么sys.modules里面就一定有module.a,和module。其实理论上有包module就够了,但是我们说a是一个模块,为了避免多次导入所以也要加到sys.modules里面,而且a又是module包里面,所以是module.a。而我们这里as?A,那么A这个符号就暴露在了当前模块的local空间里面,而且这个A就跟之前的module.a一样,指向了module包下面的a模块,无非是名字不同罢了。当然这不是重点,我们之前通过import?module.a的时候,会自动把module也加入到当前模块的local空间里面,也就是说通过import?module.a是可以直接使用module的,但是当我们加上了as之后,发现module包已经不能访问了。尽管都在sys.modules里面,但是对于加了as来说,此时的module这个包已经不在local命名空间里面了。一个as关键字,导致了两者的不同,这是什么原因呢?我们后面分解。

15.2.6 符号的销毁与重载

为了使用一个模块,无论是内置的还是自己写的,都需要import动态加载到内存,使用之后,我们也可能会删除。删除的原因一般是释放内存啊等等。在python中,删除一个对象可以使用del关键字,遇事不决del。

l = [1, 2, 3]
d = {"a": 1, "b": 2}

del l[0]
del d["a"]

print(l)  # [2, 3]
print(d)  # {'b': 2}

class A:

    def foo(self):
        pass

print("foo" in dir(A))  # True
del A.foo
print("foo" in dir(A))  # False

不光是列表、字典,好多东西del都能删除,甚至是删除某一个位置的值、或者方法。我们看到类的一个方法居然也能使用del删除,但是对于module来说,del能做到吗?显然是可以做到的,或者更准确的说法是符号的销毁符号关联的对象的销毁是一个概念吗?python已经向我们隐藏了太多的动作,也采取了太多的缓存策略,当然对于python的使用者来说是好事情,因为把复杂的特性隐藏起来了,但是当我们想彻底的了解python的行为时,则必须要把这些隐藏的东西挖掘出来。

import module.a as A

# 对于模块来说,dir()和locals()、globals()的keys是一致的
print("A" in dir())  # True
del A
print("A" in locals())  # False

import sys
print(id(sys.modules["module.a"]))  # 2985809163824

import module.a as 我不叫A了
print(id(我不叫A了))  # 2985809163824

我们看到在del之后,A这个符号确实从local空间消失了,或者说dir已经看不到了。但是后面我们发现,消失的仅仅是A这个符号,至于module.a这个PyModuleObject依旧在sys.modules里面岿然不动。然而,尽管它还存在于python系统中,但是我们的程序再也无法感知到,但它就在那里不离不弃。所以此时python就成功地向我们隐藏了这一切,我们的程序认为:module.a已经不存在了

不过为什么python要采用这种看上去类似模块池的缓存机制呢?因为组成一个完整系统的多个py文件可能都要对某个module进行import动作。所以要是从sys.modules里面删除了,那么就意味着需要重新从文件里面读取,如果不删除,那么只需要从sys.modules里面暴露给当前的local命名空间即可。所以import实际上并不等同我们所说的动态加载,它的真实含义是希望某个模块被感知,也就是将这个模块以某个符号的形式引入到某个命名空间。这些都是同一个模块,如果import等同于动态加载,那么python对同一个模块执行多次动态加载,并且内存中保存一个模块的多个镜像,这显然是非常愚蠢的。

所以python引入了全局的module集合--sys.modules,这个集合作为模块池,保存了模块的唯一值。当某个模块通过import声明希望感知到某个module时,python将在这个池子里面查找,如果被导入的模块已经存在于池子中,那么就引入一个符号到当前模块的命名空间中,并将其关联到导入的模块,使得被导入的模块可以透过这个符号被当前模块(都是执行import导入的模块)感知到。而如果被导入的模块不在池子里,python这才执行动态加载的动作。

如果这样的话,难道一个模块在被加载之后,就不能改变了。假如在加载了模块a的时候,如果我们修改了模块a,难道python程序只能先暂停再重启吗?显然不是这样的,python的动态特性不止于此,它提供了一种重新加载的机制,使用importlib模块,通过importlib.reload(module),可以实现重新加载并且这个函数是有返回值的,返回加载之后的模块。

首先我们的a模块里面啥也没有,所以dir(a)只是显示几个带有双下划线的属性,但是我们在a.py里面增加了name和age变量,然后重新加载模块,dir(a)显示多个name和age两个属性。然后我们在a.py中删除age属性、增加一个gender属性,再重新加载,dir(a)查看,首先gender属性确实加进来了了,但是我们发现age还在里面。理论上age属性应该从dir(a)里面消失才对啊,那这个age它又能不能调用呢?

并且我们看到,此时我们依然可以通过a来调用age属性。那么根据这个现象我们是不是可以大胆猜测,python在reload一个模块的时候,只是将模块里面新的符号加载进来,而删除的则不管了,那么这个猜测到底正不正确呢,别急我们后续揭晓。我们下面先通过源码来剖析一下import的实现机制。

15.3 import机制的实现

从前面的黑盒探测我们已经对import机制有了一个非常清晰的认识,python的import机制基本上可以切分为三个不同的功能。

  • python运行时的全局模块池的维护和搜索
  • 解析与搜索模块路径的树状结构
  • 对不同文件格式的模块的动态加载机制

尽管import的表现形式千变万化,但是都可以归结为:import?x.y.z的形式。因为import?sys也可以看成是x.y.z的一种特殊形式。而诸如from、as与import的结合,实际上同样会进行import?x.y.z的动作,只是最后在当前命名空间中引入符号时各有不同。所以我们就以import x.y.z的形式来进行分析。

我们说导入模块,是调用__import__,那么我们就来看看这个函数长什么样子

static PyObject *
builtin___import__(PyObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"name", "globals", "locals", "fromlist",
                             "level", 0};
    //初始化globals、fromlist都为NULL,
    PyObject *name, *globals = NULL, *locals = NULL, *fromlist = NULL;
    //表示默认绝对导入
    int level = 0;

    //从tuple中解析出需要的信息
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "U|OOOi:__import__",
                    kwlist, &name, &globals, &locals, &fromlist, &level))
        return NULL;
    //导入模块
    return PyImport_ImportModuleLevelObject(name, globals, locals,
                                            fromlist, level);
}

另外,PyArg_ParseTupleAndKeywords这个函数在python中是一个被广泛使用的函数,原型如下:

//Python/getargs.c
int PyArg_ParseTupleAndKeywords(PyObject *, PyObject *,
                                const char *, char **, ...);

这个函数的目的是将args和kwds中所包含的所有对象按format中指定的格式解析成各种目标对象,可以是python中的对象(PyListObject、PyLongObject等等),也可以是C的原生对象。

我们知道这个args实际上是一个PyTupleObject对象,包含了__import__函数运行所需要的参数和信息,它是python虚拟机在执行IMPORT_NAME的时候打包而产生的,然而在这里,python虚拟机进行了一个逆动作,将打包后的这个PyTupleObject拆开,重新获得当初的参数。python在自身的实现中大量的使用了这样打包、拆包的策略,使得可变数量的对象能够很容易地在函数之间传递。

在解析参数的过程中,指定解析格式的format中可用的格式字符有很多,这里只看一下__import__用到的格式字符。其中s代表目标对象是一个char?*,通常用来将tuple中的PyUnicodeObject对象解析成char?*,i则用来将tuple中的PyLongObject解析成int,而O则代表解析的目标对象依然是一个python中的合法对象,通常这表示PyArg_ParseTupleAndKeywords不进行任何的解析和转换,因为在PyTupleObject对象中存放的肯定是一个python的合法对象。至于|和:,它们不是非格式字符,而是指示字符,|指示其后所带的格式字符是可选的。也就是说,如果args中只有一个对象,那么__import__PyArg_ParseTupleAndKeywords的调用也不会失败。其中,args中的那个对象会按照s的指示被解析为char?*,而剩下的global、local、fromlist则会按照O的指示被初始化为Py_None,level是0。而:则指示"格式字符"到此结束了,其后所带字符串用于在解析过程中出错时,定位错误的位置所使用的。

在完成了对参数的拆包动作之后,然后进入了PyImport_ImportModuleLevelObject,这个我们在import_name中已经看到了,而且它也是先获取__builtin__里面的__import__函数指针。

PyObject *
PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
                                 PyObject *locals, PyObject *fromlist,
                                 int level)
{
    _Py_IDENTIFIER(_handle_fromlist);
    PyObject *abs_name = NULL;
    PyObject *final_mod = NULL;
    PyObject *mod = NULL;
    PyObject *package = NULL;
    PyInterpreterState *interp = PyThreadState_GET()->interp;
    int has_from;

    //name为空直接报错
    if (name == NULL) {
        PyErr_SetString(PyExc_ValueError, "Empty module name");
        goto error;
    }
    //那么必须是字符串
    if (!PyUnicode_Check(name)) {
        PyErr_SetString(PyExc_TypeError, "module name must be a string");
        goto error;
    }
    if (PyUnicode_READY(name) < 0) {
        goto error;
    }
    //level必须大于等0
    if (level < 0) {
        PyErr_SetString(PyExc_ValueError, "level must be >= 0");
        goto error;
    }
    //name大于0,获取父级目录
    if (level > 0) {
        abs_name = resolve_name(name, globals, level);
        if (abs_name == NULL)
            goto error;
    }
    //否则name是其本身
    else {  /* level == 0 */
        if (PyUnicode_GET_LENGTH(name) == 0) {
            PyErr_SetString(PyExc_ValueError, "Empty module name");
            goto error;
        }
        abs_name = name;
        Py_INCREF(abs_name);
    }

    //传入abs_name,获取模块,这里调用了PyImport_GetModule
    mod = PyImport_GetModule(abs_name);
    if (mod == NULL && PyErr_Occurred()) {
        goto error;
    }
    //如果这个mod不是NULL也不是Py_None
    if (mod != NULL && mod != Py_None) {
        _Py_IDENTIFIER(__spec__);
        _Py_IDENTIFIER(_initializing);
        _Py_IDENTIFIER(_lock_unlock_module);
        PyObject *value = NULL;
        PyObject *spec;
        int initializing = 0;

        /* Optimization: only call _bootstrap._lock_unlock_module() if
           __spec__._initializing is true.
           NOTE: because of this, initializing must be set *before*
           stuffing the new module in sys.modules.
         */
        //设置模块的__spec__属性,这个__spec__记录模块的详细信息,是一个ModuleSpec对象
        spec = _PyObject_GetAttrId(mod, &PyId___spec__);
        if (spec != NULL) {
            value = _PyObject_GetAttrId(spec, &PyId__initializing);
            Py_DECREF(spec);
        }
        if (value == NULL)
            PyErr_Clear();
        else {
            initializing = PyObject_IsTrue(value);
            Py_DECREF(value);
            if (initializing == -1)
                PyErr_Clear();
            if (initializing > 0) {
                //这里是要拿到锁
                //python虚拟机在import之前,会对import这个动作上锁。
                //目的就是为了保证多个线程对同一个模块时进行import时,不会出岔子
                //如果没有这个同步锁,那么可能会产生一些异常现象
                //当然在import结束之后,还是开锁
                value = _PyObject_CallMethodIdObjArgs(interp->importlib,
                                                &PyId__lock_unlock_module, abs_name,
                                                NULL);
                if (value == NULL)
                    goto error;
                Py_DECREF(value);
            }
        }
    }
    else {
        Py_XDECREF(mod);
        mod = import_find_and_load(abs_name);
        if (mod == NULL) {
            goto error;
        }
    }

    has_from = 0;
    //fromlist是一个tuple
    if (fromlist != NULL && fromlist != Py_None) {
        has_from = PyObject_IsTrue(fromlist);
        if (has_from < 0)
            goto error;
    }
    if (!has_from) {
        //在package的__init__中进行import动作
        Py_ssize_t len = PyUnicode_GET_LENGTH(name);
        if (level == 0 || len > 0) {
            Py_ssize_t dot;

            dot = PyUnicode_FindChar(name, '.', 0, len, 1);
            if (dot == -2) {
                goto error;
            }

            if (dot == -1) {
                /* No dot in module name, simple exit */
                final_mod = mod;
                Py_INCREF(mod);
                goto error;
            }

            if (level == 0) {
                PyObject *front = PyUnicode_Substring(name, 0, dot);
                if (front == NULL) {
                    goto error;
                }

                final_mod = PyImport_ImportModuleLevelObject(front, NULL, NULL, NULL, 0);
                Py_DECREF(front);
            }
            else {
                Py_ssize_t cut_off = len - dot;
                Py_ssize_t abs_name_len = PyUnicode_GET_LENGTH(abs_name);
                PyObject *to_return = PyUnicode_Substring(abs_name, 0,
                                                        abs_name_len - cut_off);
                if (to_return == NULL) {
                    goto error;
                }

                final_mod = PyImport_GetModule(to_return);
                Py_DECREF(to_return);
                if (final_mod == NULL) {
                    if (!PyErr_Occurred()) {
                        PyErr_Format(PyExc_KeyError,
                                     "%R not in sys.modules as expected",
                                     to_return);
                    }
                    goto error;
                }
            }
        }
        else {
            final_mod = mod;
            Py_INCREF(mod);
        }
    }
    else {
        final_mod = _PyObject_CallMethodIdObjArgs(interp->importlib,
                                                  &PyId__handle_fromlist, mod,
                                                  fromlist, interp->import_func,
                                                  NULL);
    }

  error:
    Py_XDECREF(abs_name);
    Py_XDECREF(mod);
    Py_XDECREF(package);
    if (final_mod == NULL)
        remove_importlib_frames();
    return final_mod;
}

其实每一个包和模块都有一个__name____path__属性

import numpy as np
import numpy.core
import six

print(np.__name__, np.__path__)  # numpy ['C:\\python37\\lib\\site-packages\\numpy']
print(np.core.__name__, np.core.__path__)  # numpy.core ['C:\\python37\\lib\\site-packages\\numpy\\core']
print(six.__name__, six.__path__)  # six []

__name__就是模块或者包名,如果包下面的包或者模块,那么就是包名.包名或者包名.模块名,至于__path__则是包所在的路径,但是这个和__file__又是不一样的,如果是__file__则是指向内部的__init__.py文件,没有则为None。但是对于模块来说,则没有__path__

精力有限,具体的不再深入。我们下面从python的角度来理解一下吧

15.4 python中的import操作

15.4.1 import module

import sys
"""
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sys)
              6 STORE_NAME               0 (sys)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE
"""

这是我们一开始考察的例子,现在我们已经很清楚地了解了IMPORT_NAME的行为,在IMPORT_NAME指令的最后,python虚拟机会将PyModuleObject对象压入到运行时栈内,随后会将(sys, PyModuleObject)存放到当前的local命名空间中。

15.4.2 import package

import sklearn.linear_model.ridge
"""
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sklearn.linear_model.ridge)
              6 STORE_NAME               1 (sklearn)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE
"""

如果涉及的是对package的import动作,那么IMPORT_NAME的指令参数则是关于module的完整路径信息,IMPORT_NAME指令的内部将解析这个路径,并为sklearnsklearn.linear_modelsklearn.linear_model.ridge都创建一个PyModuleObject对象,这三者都存在于sys.modules里面。但是我们看到STORE_NAME是sklearn,表示只有sklearn对应的PyModuleObject放在了当前模块的local空间里面,可为什么是sklearn呢?难道不应该是sklearn.linear_model.ridge吗?其实经过我们之前的分析这一点已经不再是问题了,因为import sklearn.linear_model.ridge并不是说导入一个模块或包叫做sklearn.linear_model.ridge,而是先导入sklearn,然后把linear_model放在sklearn的属性字典里面,把ridge放在linear_model的属性字典里面。同理sklearn.linear_model.ridge代表的是先从local空间里面找到sklearn,再从sklearn的属性字典中找到linear_model,然后在linear_model的属性字典里面找到ridge。而我们说,linear_modelridge已经在对应的包的属性字典里面的了,我们通过sklearn一级一级往下找是可以找到的,因此只需要把skearn返回即可,或者说返回sklearn.linear_model.ridge本身就是不合理的,因为这表示导入一个名字就叫做sklearn.linear_model.ridge的模块或者包,但显然不存在,即便我们创建了,但是由于python的语法解析规范依旧不会得到想要的结果。不然的话,假设import?module.a,那python是导入名为module.a的模块或包呢?还是导入module下的a呢?

也正如我们之前分析的module.a,我们import?module.a的时候,会把module加载进来,然后把a加到module的属性字典里面,然后只需要把module返回即可,因为我们通过module是可以找到a,而且也不存在我们期望的module.a,因为这个module.a代表的含义是从module的属性字典里面获取a,所以import?module.a是必须要返回module的,而且只返回了module。至于sys.modules(一个字典)里面是存在字符串名为module.a的key的,这是为了避免重复导入所采取的策略,但它依旧表示从module里面获取a。

import pandas.core

print(pandas.DataFrame({"a": [1, 2, 3]}))
"""
   a
0  1
1  2
2  3
"""
# 导入pandas.core会先执行pandas的__init__文件
# 所有通过pandas.DataFrame是可以调用的

15.4.3 from & import

from sklearn.linear_model import ridge
"""
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (('ridge',))
              4 IMPORT_NAME              0 (sklearn.linear_model)
              6 IMPORT_FROM              1 (ridge)
              8 STORE_NAME               1 (ridge)
             10 POP_TOP
             12 LOAD_CONST               2 (None)
             14 RETURN_VALUE
"""

注意此时的LOAD_CONST?1不再是None了,而是一个tuple。此时python是将ridge放到了当前模块的local空间中,并且sklearn、sklearn.linear_model都被导入了,并且存在于sys.modules里面,但是sklearn却并不在当前local空间中,尽管这个对象被创建了,但是它被python隐藏了。IMPORY_NAME是sklearn.linear_model,也表示导入sklearn,然后把sklearn下面的linear_model加入到sklearn的属性字典里面。其实sklearn没在local空间里面,还可以这样理解。只有import的时候,那么我们必须从头开始一级一级向下调用,所以顶层的包必须加入到local空间里面,但是from sklearn.linear_model import ridge是把ridge导出,此时ridge已经指向了sklearn下面的linear_model下面的ridge,那么此时就不需要sklearn了,或者说sklearn就没必要暴露在local空间里面了。并且sys.modules里面也不存在ridge这个key,存在的是sklearn.linear_model.ridge,暴露给当前模块的local空间里面的符号是ridge

15.4.4 import & as

import sklearn.linear_model.ridge as xxx
"""
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sklearn.linear_model.ridge)
              6 IMPORT_FROM              1 (linear_model)
              8 ROT_TWO
             10 POP_TOP
             12 IMPORT_FROM              2 (ridge)
             14 STORE_NAME               3 (xxx)
             16 POP_TOP
             18 LOAD_CONST               1 (None)
             20 RETURN_VALUE

"""

这个和带有from的import类似,sklearnsklearn.linear_modelsklearn.linear_model.ridge都在sys.modules里面,但是我们加上了as xxx,那么这个xxx就直接指向了sklearn下面的linear_model下面的ridge,就不需要sklearn了。这个和上面的from & import类似,只有xxx暴露在了当前模块的local空间里面,sklearn虽然在sys.modules里面,但是在当前模块就无法访问了。

15.5.5 from & import & as

from sklearn.linear_model import ridge as xxx

这个我想连字节码都不需要贴了,和之前from & import一样,只是最后暴露给当前模块的local空间的ridge变成了我们自己指定的xxx

15.5 与module有关的命名空间问题

同函数、类一样,每个PyModuleObject也是有自己的命名空间的,举个例子。

# module/a.py
name = "hanser"

def print_name():
    print(name)

# b.py, b和module同级
from module.a import name, print_name
name = "yousa"  # 将name改为"yousa"
print_name()

执行b.py,会发现打印的依旧是"hanser",我们说python是根据LEGB规则,而print_name里面没有name,那么去外层找,b.py里面的name是"yousa",但是找到的依旧是a.py里面的"hanser"。为什么?

其实用我们之前的结论依旧可以解释的通,from module.a?import?name,?print_name,这个print_name指向的是a.py里面的print_name,尽管它被导入到了b.py中,但是它记得自己从何处来,所以print(name)的时候,打印的依旧是a.py里面name。可以认为导入的模块,不管是import、还是from&import,导入过来的本身自带一层作用域,所以这个print_name是在a.py里面,尽管它是在b.py里面被执行的。如果a.py里面没有name呢?那么不好意思会报错,不会到b.py里面去找name,因为作用按照LEGB规则,但是它是没有办法跨模块的。不可能说,a.py里面LEGB找不到name,被导入到b.py里面之后,就会从b.py的LEGB去找name,这是不存在的。每个模块都有自己的命名空间(共享__builtins__),属性的查找是无法跨越模块的。

当a.py里面的属性print_name被导入到b.py中,尽管它是在b里面,但是你可以看做这个print_name四周有一堵墙,这个墙就是a.py施加的,导致print_name查找属性只能在a.py里面查找,因为它指向的就是a.py,是不可能跨越这堵墙到b.py里面查找的。而from?module.a?import name,?print_name就等价于先导入module,然后把a放到module的属性字典里面,然后由于我们import的是模块里面的内容,所以会把a.py里面的内容拿到b.py里面执行一遍,然后把name和print_name暴露给当前模块(也就是b.py)的local空间中,这都没问题。但是,如果在a.py中存在绝对导入的import,那么此时import所在的路径就不再是a.py了,而b.py所在路径。尽管name、print_name等属性的查找还是会从a.py中查找,但是对于绝对导入的模块,则变成了从b.py中导入,因为它被拿到b.py里面执行了嘛。

所以说,命名空间是python的灵魂。

15.6 完

原文地址:https://www.cnblogs.com/traditional/p/12157807.html

时间: 2024-08-06 07:36:15

《python解释器源码剖析》第15章--python模块的动态加载机制的相关文章

《python解释器源码剖析》第17章--python的内存管理与垃圾回收

17.0 序 内存管理,对于python这样的动态语言是至关重要的一部分,它在很大程度上决定了python的执行效率,因为在python的运行中会创建和销毁大量的对象,这些都设计内存的管理.同理python还提供了了内存的垃圾回收(GC,garbage collection),将开发者从繁琐的手动维护内存的工作中解放出来.这一章我们就来分析python的GC是如何实现的. 17.1 内存管理架构 在python中内存管理机制是分层次的,我们可以看成有四层,0 1 2 3.在最底层,也就是第0层是

《python解释器源码剖析》第12章--python虚拟机中的函数机制

12.0 序 函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作.当然在调用函数时,会干什么来着.对,要在运行时栈中创建栈帧,用于函数的执行. 在python中,PyFrameObject对象就是一个对栈帧的模拟,所以我们即将也会看到,python虚拟机在执行函数调用时会动态地创建新的PyFrameObject对象.随着函数调用链的增长,这些PyFrameObject对象之间也会形成一条PyFrameObject对象链,这条链就是对象x86平台上运行时栈

《python解释器源码剖析》第13章--python虚拟机中的类机制

13.0 序 这一章我们就来看看python中类是怎么实现的,我们知道C不是一个面向对象语言,而python却是一个面向对象的语言,那么在python的底层,是如何使用C来支持python实现面向对象的功能呢?带着这些疑问,我们下面开始剖析python中类的实现机制.另外,在python2中存在着经典类(classic class)和新式类(new style class),但是到Python3中,经典类已经消失了.并且python2官网都快不维护了,因此我们这一章只会介绍新式类. 13.1 p

《python解释器源码剖析》第16章--python的多线程机制

16.0 序 在介绍多线程之前,我们要先知道线程是什么,线程是操作系统调度cpu工作的最小单元,同理进程则是操作系统资源分配的最小单元,线程是需要依赖于进程的,并且每一个进程只少有一个线程,这个线程我们称之为主线程.而主线程则可以创建子线程,一个进程中有多个线程去工作,我们就称之为多线程.关于线程,请记住两句话,这两句话我们在前面章节中也已经提过了. python中的一个线程,对应c语言中的一个线程,然后对应操作系统的一个线程,操作系统的线程我们一般称之为原生线程,这三者是一一对应的. pyth

《python解释器源码剖析》第4章--python中的list对象

4.0 序 python中的list对象,底层对应的则是PyListObject.如果你熟悉C++,那么会很容易和C++中的list联系起来.但实际上,这个C++中的list大相径庭,反而和STL中的vector比较类似 4.1 PyListObject对象 我们知道python里面的list对象是支持对元素进行增删改查等操作的,list对象里面存储的,底层无一例外都是PyObject * 指针.所以实际上我们可以这样看待python底层的PyListObject:vector<PyObject

《python解释器源码剖析》第3章--python中的字符串对象

3.0 序 我们知道python中的字符串属于变长对象,当然和int也是一样,底层的结构体实例所维护的数据的长度,在对象没有定义的时候是不知道的.当然如果是python2的话,底层PyIntObject维护的就是一个long,显然在没创建的时候就知道是1. 可变对象维护的数据的长度只能在对象创建的时候才能确定,举个例子,我们只能在创建一个字符串或者列表时,才知道它们所维护的数据的长度,在此之前,我们对此是一无所知的. 注意我们在前面提到过可变对象和不可变对象的区别,在变长对象中,实际上也可以分为

Python模块动态加载机制

本文和大家分享的主要是python中模块动态加载机制相关内容,一起来看看吧,希望对大家学习python有所帮助. import 指令 来看看 import sys 所产生的指令: co_consts : (0, None) co_names : ('sys',) 0 LOAD_CONST               0 (0) 2 LOAD_CONST               1 (None) 4 IMPORT_NAME              0 (sys) 6 STORE_NAME  

Redis源码剖析和注释(十八)--- Redis AOF持久化机制

Redis AOF持久化机制 1. AOF持久化介绍 Redis中支持RDB和AOF这两种持久化机制,目的都是避免因进程退出,造成的数据丢失问题. RDB持久化:把当前进程数据生成时间点快照(point-in-time snapshot)保存到硬盘的过程,避免数据意外丢失. AOF持久化:以独立日志的方式记录每次写命令,重启时在重新执行AOF文件中的命令达到恢复数据的目的. Redis RDB持久化机制源码剖析和注释 AOF的使用:在redis.conf配置文件中,将appendonly设置为y

Django对中间件的调用思想、csrf中间件详细介绍、Django settings源码剖析、Django的Auth模块

目录 使用Django对中间件的调用思想完成自己的功能 功能要求 importlib模块介绍 功能的实现 csrf中间件详细介绍 跨站请求伪造 Django csrf中间件 form表单 ajax csrf相关装饰器 在CBV上加csrf装饰器 Django settings源码剖析及模仿使用 Django settings源码剖析 查看内部配置文件 模仿使用 Auth模块 auth简介 auth模块常用方法 创建用户 校验用户名和密码 保存用户登录状态 判断当前用户是否登录 校验原密码 修改密