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閱讀 229,237評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,205評論 2 375

推薦閱讀更多精彩內容

  • 1.1Python中的模塊介紹和使用 有過C語言編程經驗的朋友都知道在C語言中如果要引用sqrt函數,必須用語句#...
    TENG書閱讀 431評論 0 0
  • 在Python中有一個概念叫做模塊(module),這個和C語言中的頭文件以及Java中的包很類似,比如在Pyth...
    一只寫程序的猿閱讀 4,008評論 0 3
  • 1. 標準 import Python 中所有加載到內存的模塊都放在 sys.modules 。當 import ...
    唐文閣閱讀 1,750評論 0 1
  • mac mini入手幾天(配傳統鍵盤鼠標)網上找里很多使用技巧,太多了,不好記住,而且不一定用得上,現在就將使用過...
    Isme閱讀 5,493評論 1 2
  • 周一,新的開始,早上7點到校,吃早飯,7點10分上早自習,各科課代表齊作業,我也開始了我的行動,讓政治課代表郭曉雪...
    9f450160aee6閱讀 315評論 0 1