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 sys
和my math
,也就是說,導入的都是demo5目錄下面的兩個模塊。出現這兩個不同結果就是python模塊和包導入機制導致的。下面來分析下python模塊和包導入機制。
2 Python模塊和包導入原理
python模塊和包導入函數調用路徑是builtin___import__->import_module_level->load_next->import_submodule->find_module->load_module
,本文不打算分析所有的函數,只摘出幾處關鍵代碼分析。
builtin___import__
函數解析import參數,比如import xxx
和from 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']