python中經(jīng)常用到模塊,比如
import xxx,from xxx import yyy
這樣子棘劣,里面的機制也是需要好好探究一下的俏让,這次主要從黑盒角度來探測模塊機制,源碼分析點到為止呈础,詳盡的源碼分析見陳儒大神的《python源碼剖析》第14章。
1 如何導(dǎo)入模塊
首先來看一個導(dǎo)入模塊的例子橱健。創(chuàng)建一個文件夾demo5而钞,文件夾中有如下幾個文件。
ssj@ssj-mbp ~/demo5 $ ls
__init__.py math.py sys.py test.py
根據(jù)python規(guī)則拘荡,因為文件夾下面有init.py文件臼节,因此demo5是一個包。各個文件內(nèi)容如下:
#__init__.py
import sys
import math
#math.py
print 'my math'
#sys.py
print 'my sys'
#test.py
import sys
import math
好了,問題來了网缝,當(dāng)我在demo5目錄運行python test.py
的時候巨税,會打印什么結(jié)果呢?sys模塊和math模塊會調(diào)用demo5目錄下面的還是系統(tǒng)本身的模塊呢粉臊?結(jié)果是只打印出了my math
草添,也就是說,sys模塊并不是導(dǎo)入的demo5目錄下面的sys模塊扼仲。但是远寸,如果我們不是直接運行test.py,而是導(dǎo)入整個包呢屠凶?結(jié)果大為不同驰后,當(dāng)我們在demo5上層目錄執(zhí)行import demo5
時,可以發(fā)現(xiàn)打印出了my sys
和my math
矗愧,也就是說灶芝,導(dǎo)入的都是demo5目錄下面的兩個模塊。出現(xiàn)這兩個不同結(jié)果就是python模塊和包導(dǎo)入機制導(dǎo)致的唉韭。下面來分析下python模塊和包導(dǎo)入機制夜涕。
2 Python模塊和包導(dǎo)入原理
python模塊和包導(dǎo)入函數(shù)調(diào)用路徑是builtin___import__->import_module_level->load_next->import_submodule->find_module->load_module
,本文不打算分析所有的函數(shù)纽哥,只摘出幾處關(guān)鍵代碼分析钠乏。
builtin___import__
函數(shù)解析import參數(shù),比如import xxx
和from yyy import xxx
解析后獲取的參數(shù)是不一樣的春塌。然后通過import_module_level
函數(shù)解析模塊和包的樹狀結(jié)構(gòu)晓避,并調(diào)用load_next
來導(dǎo)入模塊。而load_next
調(diào)用import_submodule
來查找并導(dǎo)入模塊只壳。注意到如果是從包里面導(dǎo)入模塊的話俏拱,load_next
會先用包含包名的完整模塊名調(diào)用import_submodule
來尋找并導(dǎo)入模塊,如果找不到吼句,則只用模塊名來尋找并導(dǎo)入模塊锅必。import_submodule
會先根據(jù)模塊完整名fullname來判斷是否是系統(tǒng)模塊,即之前說過的sys.modules是否有該模塊惕艳,比如sys搞隐,os等模塊,如果是系統(tǒng)模塊远搪,則直接返回對應(yīng)模塊劣纲。否則根據(jù)模塊路徑調(diào)用find_module
搜索模塊并調(diào)用load_module
函數(shù)導(dǎo)入模塊。注意到如果不是從包中導(dǎo)入模塊谁鳍,find_module中會判斷模塊是否是內(nèi)置模塊或者擴展模塊(注意到這里的內(nèi)置模塊和擴展模塊是指不常用的系統(tǒng)模塊癞季,比如imp和math模塊等)劫瞳,如果是則直接初始化該內(nèi)置模塊并加入到之前的備份模塊集合extensions中。否則需要先后搜索模塊包的路徑和系統(tǒng)默認路徑是否有該模塊绷柒,如果都沒有搜索到該模塊志于,則報錯。找到了模塊废睦,則初始化模塊并將模塊引用加入到sys.modules
中伺绽。
load_module
這個函數(shù)需要額外說明下,該函數(shù)會根據(jù)模塊類型不同來使用不同的加載方式郊楣,基本類型有PY_SOURCE, PY_COMPILED,C_BUILTIN, C_EXTENSION,PKG_DIRECTORY等憔恳。PY_SOURCE指的就是普通的py文件,而PY_COMPILED則是指編譯后的pyc文件净蚤,如果py文件和pyc文件都存在钥组,則這里的類型為PY_SOURCE,你可能會有點納悶了,這樣豈不是影響效率了么今瀑?其實不然程梦,這是為了保證導(dǎo)入的是最新的模塊代碼,因為在load_source_module中會判斷pyc文件是否過時橘荠,如果沒有過時屿附,還是會在這里導(dǎo)入pyc文件的,所以性能上并不會有太多影響哥童。而C_BUILTIN指的是系統(tǒng)內(nèi)置模塊挺份,比如imp模塊,C_EXTENSION指的是擴展模塊贮懈,通常是以動態(tài)鏈接庫形式存在的匀泊,比如math.so模塊。PKG_DIRECTORY則是指導(dǎo)入的是包朵你,比如導(dǎo)入demo5包各聘,會先導(dǎo)入包demo5本身,然后導(dǎo)入init.py模塊抡医。
/*load_next函數(shù)部分代碼*/
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;
}
接下來就需要解釋下第一節(jié)中提出的問題了躲因,首先直接python test.py
的時候,那么先后導(dǎo)入sys模塊和math模塊
忌傻,由于是直接導(dǎo)入模塊大脉,則全名就是sys,在導(dǎo)入sys模塊的時候水孩,雖然當(dāng)前目錄下有sys模塊镰矿,但是sys模塊是系統(tǒng)模塊,所以會在import_submodule中直接返回系統(tǒng)的sys模塊荷愕。而math模塊不是系統(tǒng)預(yù)先加載的模塊衡怀,所以會在當(dāng)前目錄下找到并加載。
而如果使用了包機制安疗,我們import demo5
時抛杨,則此時會先加載demo5包本身,然后加載__init__.py
模塊荐类,init.py中會加載sys和math模塊怖现,由于是通過包來加載,所以fullname會變成demo5.sys和demo5.math玉罐。顯然在判斷的時候屈嗤,demo5.sys不在系統(tǒng)預(yù)先加載的模塊sys.modules中,因此最終會加載當(dāng)前目錄下面的sys模塊吊输。math則跟前面情況類似饶号。
3 模塊和名字空間
在導(dǎo)入模塊的時候,會在名字空間中引入對應(yīng)的名字季蚂。注意到導(dǎo)入模塊和設(shè)置的名字空間的名字時不一樣的茫船,需要注意區(qū)分下。下面給個栗子扭屁,這里有個包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'
我們知道料滥,導(dǎo)入的模塊都會加入到sys.modules
字典中然眼。當(dāng)我們導(dǎo)入模塊的時候,可以簡單分為以下幾種情況葵腹,具體原理可以參見源碼:
- import foobar.a
這是直接導(dǎo)入模塊a高每,那么在sys.modules中存在foobar和foobar.a,但是在local名字空間中只存在foobar礁蔗,并沒有foobar.a觉义。這是由import機制決定的,在導(dǎo)入模塊的代碼中可以看到針對foobar.a最終存儲到名字空間的只有foobar浴井。 - from foobar import b
這種情況存儲到sys.modules的也只有foobar(前面已經(jīng)導(dǎo)入不會重復(fù)導(dǎo)入了)和foobar.b晒骇。local名字空間只有b,沒有foobar磺浙,也沒有foobar.b洪囤。 - import foobar.a as A
這種情況sys.modules中還是foobar和foobar.a,而local名字空間只有A撕氧,沒有foobar瘤缩,更沒有foobar.a。如果我們執(zhí)行del A
刪除符號A伦泥,則名字空間不在有符號A剥啤,但是在sys.modules中還是存在foobar和foobar.a的锦溪。
4 其他
需要提到的一點是,如果我們修改了某個py文件府怯,然后reload該模塊刻诊,則刪除的符號并不會更新,而只是會加入新增加的符號或者更新已經(jīng)有的符號牺丙。比如下面這個例子则涯,我們加入 b = 2后reload模塊reloadtest,可以看到模塊中多了符號b冲簿,而我們刪除b = 2加入c=3后粟判,發(fā)現(xiàn)符號b還是在模塊reloadtest中,并沒有刪除峦剔。這是python內(nèi)部reload機制決定的档礁,在reload操作時,python內(nèi)部實現(xiàn)是找到原模塊的字典吝沫,并更新或添加符號事秀,并不刪除原有的符號。
#初始代碼 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']