Python源码剖析笔记5-模块机制

python中经常用到模块,比如import xxx,from xxx import yyy这样子,里面的机制也是需要好好探究一下的,这次主要从黑盒角度来探测模块机制,源码分析点到为止,详尽的源码分析见陈儒大神的《python源码剖析》第14章。

1 如何导入模块

首先来看一个导入模块的例子。创建一个文件夹demo5,文件夹中有如下几个文件。

ssj@ssj-mbp ~/demo5 $ ls
__init__.py math.py     sys.py      test.py

根据python规则,因为文件夹下面有init.py文件,因此demo5是一个包。各个文件内容如下:

#__init__.py
import sys
import math

#math.py
print 'my math'

#sys.py
print 'my sys'

#test.py
import sys
import math

好了,问题来了,当我在demo5目录运行python test.py的时候,会打印什么结果呢?sys模块和math模块会调用demo5目录下面的还是系统本身的模块呢?结果是只打印出了my math,也就是说,sys模块并不是导入的demo5目录下面的sys模块。但是,如果我们不是直接运行test.py,而是导入整个包呢?结果大为不同,当我们在demo5上层目录执行import demo5时,可以发现打印出了my sysmy math,也就是说,导入的都是demo5目录下面的两个模块。出现这两个不同结果就是python模块和包导入机制导致的。下面来分析下python模块和包导入机制。

2 Python模块和包导入原理

python模块和包导入函数调用路径是builtin___import__->import_module_level->load_next->import_submodule->find_module->load_module,本文不打算分析所有的函数,只摘出几处关键代码分析。

builtin___import__函数解析import参数,比如import xxxfrom yyy import xxx解析后获取的参数是不一样的。然后通过import_module_level函数解析模块和包的树状结构,并调用load_next来导入模块。而load_next调用import_submodule来查找并导入模块。注意到如果是从包里面导入模块的话,load_next先用包含包名的完整模块名调用import_submodule来寻找并导入模块,如果找不到,则只用模块名来寻找并导入模块。import_submodule会先根据模块完整名fullname来判断是否是系统模块,即之前说过的sys.modules是否有该模块,比如sys,os等模块,如果是系统模块,则直接返回对应模块。否则根据模块路径调用find_module搜索模块并调用load_module函数导入模块。注意到如果不是从包中导入模块,find_module中会判断模块是否是内置模块或者扩展模块(注意到这里的内置模块和扩展模块是指不常用的系统模块,比如imp和math模块等),如果是则直接初始化该内置模块并加入到之前的备份模块集合extensions中。否则需要先后搜索模块包的路径和系统默认路径是否有该模块,如果都没有搜索到该模块,则报错。找到了模块,则初始化模块并将模块引用加入到sys.modules中。

load_module这个函数需要额外说明下,该函数会根据模块类型不同来使用不同的加载方式,基本类型有PY_SOURCE, PY_COMPILED,C_BUILTIN, C_EXTENSION,PKG_DIRECTORY等。PY_SOURCE指的就是普通的py文件,而PY_COMPILED则是指编译后的pyc文件,如果py文件和pyc文件都存在,则这里的类型为PY_SOURCE,你可能会有点纳闷了,这样岂不是影响效率了么?其实不然,这是为了保证导入的是最新的模块代码,因为在load_source_module中会判断pyc文件是否过时,如果没有过时,还是会在这里导入pyc文件的,所以性能上并不会有太多影响。而C_BUILTIN指的是系统内置模块,比如imp模块,C_EXTENSION指的是扩展模块,通常是以动态链接库形式存在的,比如math.so模块。PKG_DIRECTORY则是指导入的是包,比如导入demo5包,会先导入包demo5本身,然后导入init.py模块。

/*load_next函数部分代码*/
static PyObject *load_next() {
    .......
    result = import_submodule(mod, p, buf); //p是模块名,buf是包含包名的完整模块名
    if (result == Py_None && altmod != mod) {
        result = import_submodule(altmod, p, p);
    }
    .......
}
/*import_submodule部分代码*/
static PyObject *
import_submodule(PyObject *mod, char *subname, char *fullname)
{
    PyObject *modules = PyImport_GetModuleDict();
    PyObject *m = NULL;

    
    if ((m = PyDict_GetItemString(modules, fullname)) != NULL) {
        Py_INCREF(m);
    }
    else {
                ......
        if (mod == Py_None)
            path = NULL;
        else {
            path = PyObject_GetAttrString(mod, "__path__");
                        ......
        }
        .......
        fdp = find_module(fullname, subname, path, buf, MAXPATHLEN+1,
                  &fp, &loader);
        .......
        m = load_module(fullname, fp, buf, fdp->type, loader);
                .......
        if (!add_submodule(mod, m, fullname, subname, modules)) {
            Py_XDECREF(m);
            m = NULL;
        }
    }
    return m;
}

接下来就需要解释下第一节中提出的问题了,首先直接python test.py的时候,那么先后导入sys模块和math模块,由于是直接导入模块,则全名就是sys,在导入sys模块的时候,虽然当前目录下有sys模块,但是sys模块是系统模块,所以会在import_submodule中直接返回系统的sys模块。而math模块不是系统预先加载的模块,所以会在当前目录下找到并加载。

而如果使用了包机制,我们import demo5时,则此时会先加载demo5包本身,然后加载__init__.py模块,init.py中会加载sys和math模块,由于是通过包来加载,所以fullname会变成demo5.sys和demo5.math。显然在判断的时候,demo5.sys不在系统预先加载的模块sys.modules中,因此最终会加载当前目录下面的sys模块。math则跟前面情况类似。

3 模块和名字空间

在导入模块的时候,会在名字空间中引入对应的名字。注意到导入模块和设置的名字空间的名字时不一样的,需要注意区分下。下面给个栗子,这里有个包foobar,里面有a.py, b.py,__init__.py

In [1]: import sys

In [2]: sys.modules['foobar']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-2-9001cd5d540a> in <module>()
----> 1 sys.modules['foobar']

KeyError: 'foobar'

In [3]: import foobar
import package foobar

In [4]: sys.modules['foobar']
Out[4]: <module 'foobar' from 'foobar/__init__.pyc'>

In [5]: import foobar.a
import module a

In [6]: sys.modules['foobar.a']
Out[6]: <module 'foobar.a' from 'foobar/a.pyc'>

In [7]: locals()['foobar']
Out[7]: <module 'foobar' from 'foobar/__init__.pyc'>

In [8]: locals()['foobar.a']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-8-059690e6961a> in <module>()
----> 1 locals()['foobar.a']

KeyError: 'foobar.a'

In [9]: from foobar import b
import module b

In [10]: locals()['b']
Out[10]: <module 'foobar.b' from 'foobar/b.pyc'>

In [11]: sys.modules['foobar.b']
Out[11]: <module 'foobar.b' from 'foobar/b.pyc'>

In [12]: sys.modules['b']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-13-1df8d2911c99> in <module>()
----> 1 sys.modules['b']

KeyError: 'b'

我们知道,导入的模块都会加入到sys.modules字典中。当我们导入模块的时候,可以简单分为以下几种情况,具体原理可以参见源码:

  • import foobar.a
    这是直接导入模块a,那么在sys.modules中存在foobar和foobar.a,但是在local名字空间中只存在foobar,并没有foobar.a。这是由import机制决定的,在导入模块的代码中可以看到针对foobar.a最终存储到名字空间的只有foobar。
  • from foobar import b
    这种情况存储到sys.modules的也只有foobar(前面已经导入不会重复导入了)和foobar.b。local名字空间只有b,没有foobar,也没有foobar.b。
  • import foobar.a as A
    这种情况sys.modules中还是foobar和foobar.a,而local名字空间只有A,没有foobar,更没有foobar.a。如果我们执行del A删除符号A,则名字空间不在有符号A,但是在sys.modules中还是存在foobar和foobar.a的。

4 其他

需要提到的一点是,如果我们修改了某个py文件,然后reload该模块,则删除的符号并不会更新,而只是会加入新增加的符号或者更新已经有的符号。比如下面这个例子,我们加入 b = 2后reload模块reloadtest,可以看到模块中多了符号b,而我们删除b = 2加入c=3后,发现符号b还是在模块reloadtest中,并没有删除。这是python内部reload机制决定的,在reload操作时,python内部实现是找到原模块的字典,并更新或添加符号,并不删除原有的符号。

#初始代码 a = 1
In [1]: import reloadtest 

In [2]: import sys

In [3]: dir(sys.modules['reloadtest'])
Out[3]: ['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'a']

##新增一行代码 b = 2 
In [4]: reload(reloadtest)
Out[4]: <module 'reloadtest' from 'reloadtest.py'>

In [5]: dir(sys.modules['reloadtest'])
Out[5]: ['__builtins__', '__doc__', '__file__', '__name__', '__package__', 'a', 'b']

##删除代码 b = 2,新增 c = 3
In [6]: reload(reloadtest)
Out[6]: <module 'reloadtest' from 'reloadtest.py'>

In [7]: dir(sys.modules['reloadtest'])
Out[7]: 
['__builtins__',
 '__doc__',
 '__file__',
 '__name__',
 '__package__',
 'a',
 'b',
 'c']
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容

  • 1.1Python中的模块介绍和使用 有过C语言编程经验的朋友都知道在C语言中如果要引用sqrt函数,必须用语句#...
    TENG书阅读 412评论 0 0
  • 在Python中有一个概念叫做模块(module),这个和C语言中的头文件以及Java中的包很类似,比如在Pyth...
    一只写程序的猿阅读 3,980评论 0 3
  • 1. 标准 import Python 中所有加载到内存的模块都放在 sys.modules 。当 import ...
    唐文阁阅读 1,732评论 0 1
  • mac mini入手几天(配传统键盘鼠标)网上找里很多使用技巧,太多了,不好记住,而且不一定用得上,现在就将使用过...
    Isme阅读 5,467评论 1 2
  • 周一,新的开始,早上7点到校,吃早饭,7点10分上早自习,各科课代表齐作业,我也开始了我的行动,让政治课代表郭晓雪...
    9f450160aee6阅读 300评论 0 1