Python源碼剖析筆記5-模塊機制

python中經常用到模塊,比如import xxx,from xxx import yyy這樣子,里面的機制也是需要好好探究一下的,這次主要從黑盒角度來探測模塊機制,源碼分析點到為止,詳盡的源碼分析見陳儒大神的《python源碼剖析》第14章。

1 如何導入模塊

首先來看一個導入模塊的例子。創(chuàng)建一個文件夾demo5,文件夾中有如下幾個文件。

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

根據python規(guī)則,因為文件夾下面有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目錄下面的還是系統(tǒng)本身的模塊呢?結果是只打印出了my math,也就是說,sys模塊并不是導入的demo5目錄下面的sys模塊。但是,如果我們不是直接運行test.py,而是導入整個包呢?結果大為不同,當我們在demo5上層目錄執(zhí)行import demo5時,可以發(fā)現打印出了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來判斷是否是系統(tǒng)模塊,即之前說過的sys.modules是否有該模塊,比如sys,os等模塊,如果是系統(tǒng)模塊,則直接返回對應模塊。否則根據模塊路徑調用find_module搜索模塊并調用load_module函數導入模塊。注意到如果不是從包中導入模塊,find_module中會判斷模塊是否是內置模塊或者擴展模塊(注意到這里的內置模塊和擴展模塊是指不常用的系統(tǒng)模塊,比如imp和math模塊等),如果是則直接初始化該內置模塊并加入到之前的備份模塊集合extensions中。否則需要先后搜索模塊包的路徑和系統(tǒng)默認路徑是否有該模塊,如果都沒有搜索到該模塊,則報錯。找到了模塊,則初始化模塊并將模塊引用加入到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指的是系統(tǒng)內置模塊,比如imp模塊,C_EXTENSION指的是擴展模塊,通常是以動態(tài)鏈接庫形式存在的,比如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;
}

接下來就需要解釋下第一節(jié)中提出的問題了,首先直接python test.py的時候,那么先后導入sys模塊和math模塊,由于是直接導入模塊,則全名就是sys,在導入sys模塊的時候,雖然當前目錄下有sys模塊,但是sys模塊是系統(tǒng)模塊,所以會在import_submodule中直接返回系統(tǒng)的sys模塊。而math模塊不是系統(tǒng)預先加載的模塊,所以會在當前目錄下找到并加載。

而如果使用了包機制,我們import demo5時,則此時會先加載demo5包本身,然后加載__init__.py模塊,init.py中會加載sys和math模塊,由于是通過包來加載,所以fullname會變成demo5.sys和demo5.math。顯然在判斷的時候,demo5.sys不在系統(tǒng)預先加載的模塊sys.modules中,因此最終會加載當前目錄下面的sys模塊。math則跟前面情況類似。

3 模塊和名字空間

在導入模塊的時候,會在名字空間中引入對應的名字。注意到導入模塊和設置的名字空間的名字時不一樣的,需要注意區(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'

我們知道,導入的模塊都會加入到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。如果我們執(zhí)行del A刪除符號A,則名字空間不在有符號A,但是在sys.modules中還是存在foobar和foobar.a的。

4 其他

需要提到的一點是,如果我們修改了某個py文件,然后reload該模塊,則刪除的符號并不會更新,而只是會加入新增加的符號或者更新已經有的符號。比如下面這個例子,我們加入 b = 2后reload模塊reloadtest,可以看到模塊中多了符號b,而我們刪除b = 2加入c=3后,發(fā)現符號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']
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

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