《python解釋器源碼剖析》第15章--python模塊的動態加載機制


15.0 序

在之前的章節中,我們考察的東西都是局限在一個模塊(在python中就是module)內。然而現實中,程序不可能只有一個模塊,更多情況下一個程序會有多個模塊,而模塊之間存在着引用和交互,這些引用和交互也是程序的一個重要的組成部分。本章剖析的就是在python中,一個模塊是如何加載、並引用另一個模塊的功能的。對於一個模塊,肯定要先從硬盤加載到內存。

15.1 import前奏曲

我們以一個簡單的import為序幕

# a.py
import sys 
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sys)
              6 STORE_NAME               0 (sys)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE

我們發現對應的字節碼真的是簡單無比。先不管開頭的兩個LOAD_CONST,我們看到了IMPORT_NAME,這個可以類比LOAD_NAME將sys這個module加載進來,然后調用STORE_NAME存儲在當前PyFrameObject的local空間中,然后當我們調用sys.path的時候,虛擬機就能很輕松地找到sys這個符號對應的值了。因此本質上和創建一個變量是沒有什么區別的,關鍵就是這個IMPORT_NAME,我們看看它的實現,知道從哪里看嗎?我們說python中所有的指令集的實現都在ceval.c的那個大大的for循環中的大大的switch中。

        TARGET(IMPORT_NAME) {
            //PyUnicodeObject對象,比如import pandas,那么這個name就是字符串pandas
            PyObject *name = GETITEM(names, oparg);
            //我們看到這里的有一個fromlist和level,得到的結果是None和0
            //我們再看一下剛才的字節碼,我們發現在IMPORT_NAME之前有兩個LOAD_CONST,將0和None壓入了運行時棧
            //顯然這里是將運行時棧中的內容彈出來,分別賦值給fromlist和level
            PyObject *fromlist = POP(); //None
            PyObject *level = TOP(); // 0
            PyObject *res;
            // 調用了import_name,然后返回res
            res = import_name(f, name, fromlist, level);
            Py_DECREF(level);
            Py_DECREF(fromlist);
            //將res壓入運行時棧
            SET_TOP(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }

因此重點在import_name這個函數中,但是在此之前我們需要重點關注一下這個fromlist和level,而這一點我們從python的層面來介紹。我們知道在python中,我們導入一個模塊直接通過import sys即可, 但是除了import,我們還可以使用__import__,這個__import__是解釋器使用的一個函數,不推薦我們直接使用,但是我想說的是import os等價於os = __import__("os")

os = __import__("os")
print(os)  # <module 'os' from 'C:\\python37\\lib\\os.py'>

# 當然我們使用__import__的時候,還可以使用其他的名字
aaa = __import__("sys")
print(aaa.prefix)  # C:\python37

但是問題來了

m = __import__("os.path")
print(m)  # <module 'os' from 'C:\\python37\\lib\\os.py'>

# 我們驚奇地發現,居然還是os模塊,按理說應該是os.path(windows系統對應ntpath)啊
m1 = __import__("os.path", fromlist=[""])
print(m1)  # <module 'ntpath' from 'C:\\python37\\lib\\ntpath.py'>

# 你看到了什么,fromlist,沒錯,我們加上一個fromlist,就能導入子模塊了

為什么會這樣呢?我們來看看__import__這個函數的解釋,這個是pycharm給抽象出來的。

def __import__(name, globals=None, locals=None, fromlist=(), level=0): 
    """
    __import__(name, globals=None, locals=None, fromlist=(), level=0) -> module
    
    Import a module. Because this function is meant for use by the Python
    interpreter and not for general use, it is better to use
    importlib.import_module() to programmatically import a module.
    
    The globals argument is only used to determine the context;
    they are not modified.  The locals argument is unused.  The fromlist
    should be a list of names to emulate ``from name import ...'', or an
    empty list to emulate ``import name''.
    When importing a module from a package, note that __import__('A.B', ...)
    returns package A when fromlist is empty, but its submodule B when
    fromlist is not empty.  The level argument is used to determine whether to
    perform absolute or relative imports: 0 is absolute, while a positive number
    is the number of parent directories to search relative to the current module.
    """
    pass

大意就是,此函數會由import語句調用,當我們import一個模塊的時候,解釋器底層就會調用__import__import os表示將"os"這個字符串傳入__import__中,導入os模塊,並將返回值再次賦值給符號os,也就是os = __import__("os")。我們是可以通過這種方式來導入模塊,但是python不建議我們這么做。而globals參數則是確定import語句包的上下文,一般直接傳globals()即可,但是locals參數我們不會用,但是一般情況下globals和locals我們都不用管。

fromlist我們剛才已經說了,__import__("os.path"),如果是這種情況的話,那么導入的不是os.path,還是os這個外層模塊,如果想導入os.path,那么只需要給fromlist傳入一個非空列表即可,其實不僅僅是非空列表,只要是一個非空的可迭代對象就行。而level如果是0,那么表示僅執行絕對導入,如果是一個正整數,表示要搜索的父目錄的數量。一般這個值也不需要傳遞。

這個方法有什么作用,就是如果我們有一個字符串a,其值為pandas,我想導入這個模塊,該怎么做呢?顯然就可以使用這種方式,但是這種方式導入的話,python官方不推薦使用__import__,而是希望我們使用一個叫做importlib的模塊

import importlib

a = "pandas"
pd = importlib.import_module(a)
# 通過這種方式依舊是可以導入的
print(pd)  # <module 'pandas' from 'C:\\python37\\lib\\site-packages\\pandas\\__init__.py'>

# 另外
m = importlib.import_module("os.path")
# 我們看到這種模式是支持導入包里面的包、或者模塊的
print(m)  # <module 'ntpath' from 'C:\\python37\\lib\\ntpath.py'>

扯了這么多,我們來看看之前源碼中說的import_name

//ceval.c
// 這個函數接收了四個參數,f:棧幀,name:模塊名,fromlist:一個None,level:0
res = import_name(f, name, fromlist, level);

static PyObject *
import_name(PyFrameObject *f, PyObject *name, PyObject *fromlist, PyObject *level)
{
    _Py_IDENTIFIER(__import__);
    PyObject *import_func, *res;
    PyObject* stack[5];
	
    //獲取內建函數__import__,但是此時的__import__已經不是一個PyFunctionObject了
    //而是一個PyCFunctionObject,我們上一章分析python初始化動作時,我們看到在初始化
    //__builtin__module時,函數已經搖身一變,被包裝成PyCFunctionObject了
    import_func = _PyDict_GetItemId(f->f_builtins, &PyId___import__);
    //為NULL獲取失敗
    if (import_func == NULL) {
        PyErr_SetString(PyExc_ImportError, "__import__ not found");
        return NULL;
    }

    /* Fast path for not overloaded __import__. */
    //判斷import是否被重載了
    if (import_func == PyThreadState_GET()->interp->import_func) {
        int ilevel = _PyLong_AsInt(level);
        if (ilevel == -1 && PyErr_Occurred()) {
            return NULL;
        }
        //未重載的話,調用PyImport_ImportModuleLevelObject
        res = PyImport_ImportModuleLevelObject(
                        name,
                        f->f_globals,
                        f->f_locals == NULL ? Py_None : f->f_locals,
                        fromlist,
                        ilevel);
        return res;
    }

    Py_INCREF(import_func);

    stack[0] = name;
    stack[1] = f->f_globals;
    stack[2] = f->f_locals == NULL ? Py_None : f->f_locals;
    stack[3] = fromlist;
    stack[4] = level;
    res = _PyObject_FastCall(import_func, stack, 5);
    Py_DECREF(import_func);
    return res;
}


//import.c
PyObject *
PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
                                 PyObject *locals, PyObject *fromlist,
                                 int level)
{
    _Py_IDENTIFIER(_handle_fromlist);
    PyObject *abs_name = NULL;
    PyObject *final_mod = NULL;
    PyObject *mod = NULL;
    PyObject *package = NULL;
    PyInterpreterState *interp = PyThreadState_GET()->interp;
    int has_from;
	
    //名字不可以為空
    if (name == NULL) {
        PyErr_SetString(PyExc_ValueError, "Empty module name");
        goto error;
    }

    //名字必須是PyUnicodeObject
    if (!PyUnicode_Check(name)) {
        PyErr_SetString(PyExc_TypeError, "module name must be a string");
        goto error;
    }
    //level不可以小於0
    if (level < 0) {
        PyErr_SetString(PyExc_ValueError, "level must be >= 0");
        goto error;
    }
	
    //level大於0
    if (level > 0) {
        //在相應的父目錄尋找,得到abs_name
        abs_name = resolve_name(name, globals, level);
        if (abs_name == NULL)
            goto error;
    }
    //否則的話,說明level==0,因為level要求是一個大於等於0的整數
    else {  /* level == 0 */
        if (PyUnicode_GET_LENGTH(name) == 0) {
            PyErr_SetString(PyExc_ValueError, "Empty module name");
            goto error;
        }
        //此時直接將name賦值給abs_name
        //因為此時是絕對導入
        abs_name = name;
        Py_INCREF(abs_name);
    }
	
    //調用PyImport_GetModule獲取模塊
    //注意:這個模塊會從sys.modules里面獲取,並不是直接導入模塊
    //我們說在python中,一個模塊不會被重復導入,一旦導入之后會加入到sys.modules中
    //當導入的時候,會從sys.modules里面查找,比如有一個a.py,里面寫了一個print
    //但是我們導入兩次,但是這個print只會打印一次,因此模塊只會導入一次
    mod = PyImport_GetModule(abs_name);
    if (mod == NULL && PyErr_Occurred()) {
        goto error;
    }
    ...
    ...    
    else {
        //調用函數,導入模塊
        final_mod = _PyObject_CallMethodIdObjArgs(interp->importlib,
                                                  &PyId__handle_fromlist, mod,
                                                  fromlist, interp->import_func,
                                                  NULL);
    }

  error:
    Py_XDECREF(abs_name);
    Py_XDECREF(mod);
    Py_XDECREF(package);
    if (final_mod == NULL)
        remove_importlib_frames();
    return final_mod;
}

我們知道在python中,從語法層面上來講有很多種寫法,比如:

import a
import a.b
from a import b
from a import b as bb
from a import *

從import的目標來說,可以有系統的標准模塊,還有用戶自己寫的模塊,而用戶寫的模塊又分為python原生實現的模塊和C語言實現並以dll或者so形式存在的模塊,下面我們將一一介紹。

15.2 python中import機制的黑盒探測

同golang的package、c++的namespace,python通過module機制和之后會挖掘的package機制來實現對系統復雜度的分解,以及保護命名空間不受污染。這里的module就是一個python可以導入的文件,而package是一個python可以導入的包、或者說是目錄。所以,package可以包含module

通過module和package,我們可以將某個功能、某種抽象進行獨立的實現和維護,在module和package的基礎之上構建軟件,這樣不僅使得軟件的架構清晰,而且也能很好的實現代碼復用。

15.2.1 標准import

15.2.1.1 python內建module

sys這個模塊恐怕是使用的非常頻繁的module了,我們就從這位老鐵入手。在對python運行環境的初始化分析中,我們看到了dir,這個小工具是我們探測import的殺手鐧。如果你在交互式環境下輸入dir(),那么會打印當前local命名空間的所有符號,如果有參數,則將參數視為對象,輸出該對象的所有屬性。我們先來看看import動作對當前命名空間的影響

我們看到當我們進行了import動作之后,當前的local命名空間增加了一個sys符號。而且通過type操作,我們看到這個sys符號對應這一個module對象,當然在cpython中是一個PyModuleObject。當然雖然寫着<class 'module'>,但是我們在python中是看不到的。但是它是一個class,那么就一定繼承object,並且元類為type

這與我們的分析是一致的。言歸正傳,我們看到import機制影響了當前local命名空間,使得加載的module對象在local空間成為可見的。實際上,這和我們創建一個變量的時候,也會影響local命名空間。引用該module的方法正是通過module的名字,即這里的sys。

不過這里還有一個問題,我們來看一下。

驚了,我們輸入sys,居然是builtin,是內置的。可既然如此,那為什么我們不能直接使用,還需要導入呢?其實不光是sys,在python初始化的時候,就已經將一大批的module的加載到了內存中。但是為了使得local命名空間能夠達到最干凈的效果,python並沒有講這些符號暴露在local命名空間中,而是需要用戶顯式的使用import機制來將這個符號引入到local命名空間中,以便程序能夠使用這個符號背后的對象。

我們知道,凡是加載進內存的模塊都保存在sys.modules里面,盡管local里面沒有,但是sys.modules里面是跑不掉的。

import sys
# 這個modules是一個字典,里面分別為module name和PyModuleObject
# 里面有很多模塊,我就不打印了,另外感到意味的是,居然把numpy也加載進來了
modules = sys.modules

np = modules["numpy"]
arr = np.array([1, 2, 3, 4, 5])
print(np.sum(arr))  # 15

os_ = modules["os"]
import os
print(id(os) == id(os_))  # True

一開始這些模塊名是不在local里面的,除非我們顯式導入,但是即便我們導入,這些模塊也不會被二次導入,因為已經在初始化的時候就被加載到內存里面了。因此對於已經在sys.modules里面的模塊來說,導入的時候只是加到local空間里面去了,所以代碼中的os和os_的id是一樣的。如果我們在python啟動之后,導入一個sys.modules中不存在的模塊,那么會同時進入local和sys.modules

15.2.1.2 用戶自定義module

我們知道,對於那些內建模塊(解釋器初始化的時候自動加入到sys.modules的模塊),如果import,只是將該模塊暴露在了local命名空間中。下面我們看看對於那些沒有在初始化的時候加載到內存的module進行import的時候,會出現什么樣動作。當然正如我們之前說的,用戶可以通過py文件創建自己的module、python標准庫中的.py module、第三方.py module,當然這些都是py文件,以及通過C語言創建dll、so,生成python的擴展module,這些都不是python的內建module。不過我們目前不區分py文件和dll文件,只以py文件作為例子,探探路。

# a.py
a = 1
b = 2
import sys
print("a" in sys.modules)  # False
import a
print("a" in sys.modules)  # True
print(dir())  
# ['__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'sys']

print(id(a))  # 2653299804976
print(id(sys.modules["a"]))  # 2653299804976

print(type(a))  # <class 'module'>

操作type()的結果顯示,import機制確實創建了一個新的module。而且也確實如我們之前所說python對a這個module,不僅將其引入進當前的local命名空間中,而且這個被動態加載的module也在sys.module中擁有了一席之地。並且local中符號a和sys.modules中符號a背后隱藏的是同一個PyModuleObject對象。然后我們再來看看這個module對象。

import a
print(dir(a))  # ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'a', 'b']
print(a.__dict__.keys())
# dict_keys(['__name__', '__doc__', '__package__', '__loader__', '__spec__', '__file__', '__cached__', '__builtins__', 'a', 'b'])

print(a.__name__)  # a
print(a.__file__)  # C:\Users\satori\Desktop\love_minami\a.py

這里可以看到,module對象內部實際上是通過一個dict在維護所有的{屬性: 屬性值},里面有module的元信息(名字、文件路徑)、以及module里面的內容。對,說白了,同class一樣,module又是一個命名空間

另外如果此時你查看a.py所在目錄的__pycache__目錄,你會發現里面有一個a.pyc,說明python在導入的時候先生成了pyc,然后導入了pyc。並且我們通過dir(a)查看的時候,發現里面有一個__builtins__符號,那么這個__builtins__和我們之前說的那個__builtins__是一樣的嗎?

import a

# 我們之前說獲取builtins可以通過import builtins的方式導入,但其實也可以通過__builtins__獲取
print(id(__builtins__), type(__builtins__))  # 1745602347792 <class 'module'>
print(id(a.__dict__["__builtins__"]), type(a.__dict__["__builtins__"]))  # 1745602345408 <class 'dict'>

盡管它們都叫__builtins__,但一個是module,一個是dict。我們通過__builtins__直接獲取的是一個module,里面存放了int、str、globals等內建對象和內建函數等等,我們直接輸入int、str、globals和通過—__builtins__.int__builtins__.str__builtins__.globals的效果是一樣的,我們輸入__builtins__可以拿到這個內置模塊,通過這個內置模塊去獲取里面的內容,當然也可以直接獲取里面的內容,因為這些已經是全局的了。

但是a.__dict__["__builtins__"]是一個dict,這就說明兩個從性質上就是不同的東西,但即便如此,就真的一點關系也沒有嗎?

import a

print(id(__builtins__.__dict__))  # 2791398177216
print(id(a.__dict__["__builtins__"]))  # 2791398177216

我們看到還是有一點關系的,和類、類的實例對象一樣,每一個模塊也有自己的屬性字典__dict__,記錄了自身的元信息、里面存放的內容等等,對於a.__dict__["__builtins__"]來說,拿到的就是__builtins__.__dict__,所以說__builtins__是一個模塊,但是這個模塊有一個__dict__屬性字典,而這個字典是可以通過module.__dict__["__builtins__"]來獲取的,因為任何一個模塊都可以使用__builtins__里面的內容,並且所有模塊對應的__builtins__都是一樣的。所以當你直接打印a.__dict__的時候會輸出一大堆內容,因為輸出的內容里面不僅有當前模塊的內容,還有__builtins__.__dict__。再提一遍屬性字典,當我們通過obj.attr的時候,本質上是通過obj.__dict__["attr"]獲取的。

import a

print(a.__dict__["__builtins__"]["list"]("abcd"))  # ['a', 'b', 'c', 'd']

# a.__dict__["__builtins__"]就是__builtins__.__dict__這個屬性字典
# __builtins__.list就等價於__builtins__.__dict__["list"]
# 說白了,就是我們直接輸入的list

print(a.__dict__["__builtins__"]["list"] is list)  # True

# 回顧之前的內容

# 我們說,模塊名是在模塊的屬性字典里面
print(a.__dict__["__name__"] == a.__name__ == "a")  # True

# __builtins__里面的__name__就是builtins
print(__builtins__.__dict__["__name__"])  # builtins

# 還記得如何獲取當前文件的文件名嗎
print(__name__)   # __main__
# 咦,可能有人說,這不是從__builtins__里面獲取的嗎?
# 我們之前說了,__name__已經被設置到local命名空間了
# 所以這個__name__是從local里面拿的,盡管我們沒有設置,但是它確確實實在里面
# 而且local里面有的話,就不會再去找__builtins__

15.2.2 嵌套import

我們下面來看一下import的嵌套,所謂import的嵌套就是指,假設我import a,但是在a中又import b,我們來看看這個時候會發生什么有趣的動作。

# a.py
import sys
# b.py
import a
"""
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (a)
              6 STORE_NAME               0 (a)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE
"""

在a.py中導入sys,在另一個模塊導入a,打印字節碼指令,我們只看到了IMPORT_NAME 0 (a),似乎並沒有看到a模塊中的動作。我們說了,使用dis模塊查看的字節碼是分層級的,只能看到import a這個動作,a里面做了什么是看不到的。

# b.py
import a
import sys

print(a.sys is sys is sys.modules["sys"] is a.__dict__["sys"])  # True

首先我們import a,那么a模塊就在當前模塊的屬性字典里面了,我通過a這個符號是可以直接拿到其對應的模塊的。但是在a中我們又import sys,那么這個sys模塊就已經在a模塊對應的屬性字典里面了,也就是說,我們在b.py中通過a.sys是可以直接拿到sys模塊的。但是,我們第二次導入sys的時候,會怎么樣呢?首先我們在a中已經導入了sys,那么sys這個模塊就已經在sys.modules里面了。那么當我們再導入sys的時候,就直接從sys.modules里面去找了,因此不會二次導入。為了更直觀的驗證,我們再舉一個例子:

# a.py
print(123)

# b.py
import a

# c.py
import a

以上是三個文件,我們下面再創建一個文件來導入一下:

import a
# 導入模塊就相當於把該模塊的內容拿出來,在當前模塊執行一遍
# 並且里面的內容是在被導入模塊的命名空間里面的,比如這里只能通過符號a來獲取a里面的內容
"""
123
"""
import b

"""
123
"""
import a
import b
import c

"""
123
"""

你看到了什么,我們導入a的時候,相當於把a里面內容取出來執行一遍,打印了123。導入b的時候,把b的內容拿到當前模塊里面執行,b里面又導入了a,然后又把a里面的內容取出來拿到b里面執行,依舊打印123。同理導入c也是一樣的,但是我們同時導入a、b、c三個模塊,卻只打印了一遍123,明明應該打印3次的啊。還是我們之前說的,當我在import a的時候,a這個模塊就已經在sys.modules里面了,那么當我import b、b里面 import a的時候,直接去sys.modules把a這個符號對應的模塊取出來加到模塊b的local命名空間即可,不會再進行導入了,同理c也是一樣。

所以我們發現sys.modules是一個全局的模塊空間,不管在執行程序涉及到幾個py文件,各個py文件導入了幾個模塊,只要導入就會先到sys.modules里面找,找不到再導入模塊並加入到sys.modules里面,只要sys.modules里面有,就直接取並加入到local命名空間、不會二次導入。所以每一個module的import不會影響其它的module,只會影響自身module的命名空間,或者module自身維護的那個dict對象(__dict__),local、global空間的內容在__dict__中都能找到

所以我們可以把sys.modules看成是一個大倉庫,任何導入了的模塊都在這里面。如果再導入的話,在sys.modules里面找到了,就直接返回即可,這樣可以避免重復import

15.2.3 import package

package就是所謂的包(大白話就是一個文件夾,霧),我們寫的多個邏輯或者功能上相關的函數、類可以放在一個module里面,那么多個module是不是也可以組成一個package呢?如果說module是管理class、函數、一些變量的機制,那么package就是管理module的機制,當然啦,多個小的package又可以聚合成一個較大的package。

因此在python中,module是由一個單獨的文件來實現的,可以使py文件、或者pyc文件、pyd文件、甚至是用C擴展的dll文件。而對於package來說,則是一個目錄,里面容納了py、pyc或者dll文件,這種方式就是把多個module聚合成一個package的具體實現。

現在我有一個名為package的模塊,里面有一個a.py

a.py內容如下

a = 123
b = 456
print(">>>")

現在我們來導入它

import module
print(module)  # <module 'module' (namespace)>

在python2中,這樣是沒辦法導入的,因為如果一個目錄要成為python中的package,那么里面必須要有一個__init__文件,但是在python3中沒有此要求。而且我們發現print之后,顯示這package也是一個module對象,因此python對於module和package的底層定義其實是很靈活的,並沒有那么僵硬。

import module
print(module.a)  # AttributeError: module 'module' has no attribute 'a'

然而此時神奇的地方出現了,我們調用module.a的時候,告訴我們沒有a這個屬性。很奇怪,我們的module里面不是有a.py嗎?首先python導入一個包,會先執行這個包的__init__文件,只有在__init___文件中導入了,我們才可以通過包名來調用。如果這個包里面沒有__init__文件,那么你導入這個包,是什么屬性也用不了的。光說可能比較難理解,我們來演示一下。我們先來創建__init__文件,但是里面什么也不寫。

import module
print(module)  
# <module 'module' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\__init__.py'>

此時我們又看到了神奇的地方,我們在module目錄里面創建了__init__文件之后,再打印module,得到結果就變了,告訴我們這個包來自於該包里面的__init__文件,所以就像我們之前說的,python對於包和模塊的概念區分的不是很明顯,我們把包就當做該包下面的__init__文件即可,這個__init__中定義了什么,那么這個包里面就有什么。

# module/__init__.py
import sys
from . import a
name = "satori"

from . import a這句話表示導入module這個下面的a.py,但是直接像import sys那樣import a不行嗎?答案是不行的,至於為什么我們后面說。我們在__init__.py中導入了sys模塊、a模塊,定義了name屬性,那么就等於將sys、a、name加入到了module這個包的屬性字典里面去了。因為我們說過,對於python中的包,那么其等價於里面的__init__文件,這個文件有什么,那么這個包就有什么。既然我們在__init__.py中導入了sys、a模塊,定義了name,那么這個文件的屬性字典里面、或者也可以說local空間里面就有了"sys": sys, "a": a, "name": "satori"這三個entry,而我們又說了__init__.py里面有什么,那么通過包名就能夠調用什么。

import module

print(module.a)
print(module.a.a)
print(module.sys)
print(module.name)
"""
>>>
<module 'module.a' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\a.py'>
123
<module 'sys' (built-in)>
satori
"""
# 首先在a里面有一個print(123)
# 而我們說導入一個模塊,就相當於把這個模塊拿過來執行一遍。導入一個包則是把這個包里面的`__init__`執行一遍
# 那么在__init__里面導入a的時候,就會打印這個print

# 另外此時如果我再單獨導入module里面的a模塊的話,會怎么樣呢?
# 下面這兩種導入方式后面會介紹
import module.a
from module import a
# 我們看到a里面的print沒有被打印,證明確實模塊、包不管以怎樣的方式被導入,只要被導入了,那么就只會被導入一遍

所以這個__init__.py的作用我們就很清晰了,當我們只想導入一個包的時候,那個通過這包能夠使用哪些屬性,就通過__init__.py中定義,只有在__init__中定義了,才可以通過包名直接調用。

15.2.3.1 相對導入與絕對導入

我們剛才使用了一個from . import a的方式,這個.表示當前文件所在的目錄,這行代碼就表示,我要導入a這個模塊,不是從別的地方導入,而是從該文件所在的目錄里面導入。如果是..就表示該目錄的上一層目錄,三個.、四個.依次類推。我們知道a模塊里面還有一個a這個變量,那如果我想在__init__.py中導入這個變量該怎么辦呢?直接from .a import a即可,表示導入當前目錄里面的a模塊里面的a變量。如果我們導入的時候沒有.的話,那么表示絕對導入,python虛擬機就會按照sys.path定義的路徑去找。假設我們在__init__.py當中寫的是不是from . import a,而是import a,那么會發生什么后果呢?

import module
"""
  File "C:\Users\satori\Desktop\love_minami\module\__init__.py", line 2, in <module>
    import a
ModuleNotFoundError: No module named 'a'
"""

我們發現報錯了,告訴我們沒有a這個模塊,可是我們明明在module包里面定義了呀。還記得之前說的導入一個模塊、導入一個包會做哪些事情嗎?導入一個模塊,會將該模塊里面拿過來執行一遍,導入包會將該包里面的__init__.py文件拿過來執行一遍。注意:我們把拿過來三個字加粗了。

我們注意到,這個test.py里面導入了module,那么就相當於把module目錄里面__init__.py拿到test里面來執行一遍,然后它們具有單獨的空間,是被隔離的,調用需要使用符號module來調用。但是正如我們之前所說,是拿過來執行,所以這個__init__.py里面的內容是拿過來、然后在test(在哪里導入的就是哪里)里面執行的。所以由於import a這行代碼表示絕對導入,就相當於在test模塊里面導入,會從sys.path里面搜索,但是a是在module包里面,那么此時還能找到這個a嗎?顯然是不能的。那from . import a為什么就好使呢?因為這種導入表示相對導入,就表示要在__init__.py所在目錄里面找,那么不管在什么地方導入這個包,由於這個__init__.py的位置是不變的,所以from . import a這種相對導入的方式總能找到對應的a。至於sys(標准庫、第三方包),因為它是在sys.path里面的,在哪兒都能找得到,所以可以絕對導入,貌似也只能絕對導入。並且我們知道每一個模塊都有一個__file__屬性,當然包也是。如果你在一個模塊里面print(__file__),那么不管你在哪里導入這個模塊,打印的永遠是這個模塊的地址。

另外關於相對導入,一個很重要的一點,一旦一個模塊出現了相對導入,那么這個模塊就不能被執行了,它只可以被導入。

import sys
from . import a
name = "satori"
"""
    from . import a
ImportError: attempted relative import with no known parent package
"""

此時如果我試圖執行__init__.py,那么就會給我報出這個錯誤。另外即便導入一個內部具有"相對導入"的模塊,那么此模塊和導入的模塊也不能在同一個包內,我們要執行的此模塊至少要在導入模塊的上一級,否則執行此模塊也會報出這種錯誤。為什么會有這種情況,很簡單。想想為什么會有相對導入,就是希望這些模塊在被其它地方導入的時候能夠准確記住要導入的包的位置。那么這些模塊肯定要在一個共同的包里面,然后我們在包外面使用。所以我們導入一個具有相對導入的模塊時候,那么我們當前模塊和要導入的模塊絕對不能在同一個包里面。

就像我們這樣,test.py和module包是分開的,module包里面出現了相對導入,那么它應該作為一個整體被外面的人使用,我們使用的test.py和module是同級的,那么它就比module里面的模塊高一級。並且注意的是,module包里面的相對導入不要超出module包這個范圍。我們知道.表示當前文件所在的目錄,那么n個.就表示當前的目錄的上n-1級目錄。既然是在module這個包里面,那么范圍就不要越過module這個包。

# module/__init__.py
import sys
from .. import test
name = "satori"

顯然.表示module目錄,那么..就是module所在的目錄了,它越過module目錄,在里面導入了test,這說明什么?這意味着module雖然還是一個包,但它只是一個小包,module所在目錄的包才是最終的包

也就說此時,love_minami這個目錄才是一個外層的包,module包不過是這里面的小包。我們說一個package里面是可以嵌套package的。

# 1.py
import module
"""
    from .. import test
ValueError: attempted relative import beyond top-level package
"""

如果我此時再導入module,就會報錯,這里提示我們相對導入越過了頂層的package。我們說過進行相對導入的模塊只能被其他模塊導入,不可以執行。另外,我們要執行的模塊一定要在出現了相對導入的模塊的上一級,准確的說相對導入里面的最高層級的目錄的上一層。比如__init__里面出現了..,表示love_minami這個目錄,那么你要想導入module,那么你執行的模塊至少要在..的上一級,也就是love_minami所在的目錄,因為相對導入超出了module包,那么此時love_minami就應該作為一個package,如果想導入,那么你至少要和love_minami同級。關於相對導入,解釋起來比較費勁,能大致理解就好,總之希望記住以下幾點:

  • 出現了相對導入,那么這個模塊是不能被執行的,只能被導入
  • 出現了相對導入的模塊,那么它至少要在一個包內,相對導入不能越過這個包。
  • 執行導入的模塊必須至少和這個導入的模塊所在的包是同級的。

關於相對導入,解釋起來實在不好說,有點語無倫次,或者說的不嚴謹。不過可以自己嘗試一下,還是很容易理解的。

15.2.3.2 import的另一種方式

我們要導入module包里面的a模塊,除了可以import module(_init__.py里面導入了a),還可以通過import module.a的方式,另外如果是這種導入方式,那么module里面可以沒有__init__.py文件,因為我們導入module包的時候,是通過module來獲取a,所以必須要有__init__.py文件、並且里面導入a。但是在導入module.a的時候,就是找module.a,所以此時是可以沒有__init__.py文件的

# module/__init__.py
name = "satori"

此時module包里面的__init__.py只有name這個變量,下面我們來通過module.a的形式導入。

import module.a

print(module.a.a)
"""
>>>
123
"""

# 當import module.a的時候,會執行里面的print
# 然后可以通過module.a獲取a里面的屬性,這很好理解

# 但是,沒錯,我要說但是了
print(module.name)  # satori

驚了,我們在導入module.a的時候,也把module導入進來了,為了更直觀的看到現象,我們在__init__.py里面打印一句話

# module/__init__.py
name = "satori"
print("我是module下面的__init__")
import module.a
"""
我是module下面的__init__
>>>
"""

所以一個有趣的現象就產生了,我們是導入module.a,但是把module也導入進來了。而且通過打印的順序,我們看到是先導入了module,然后在導入module下面的a。如果在__init__.py里面導入了a,那么import module.a就只會導入module,module.a就不會二次導入了

# # module/__init__.py
name = "satori"
print("我是module下面的__init__")
from . import a
import module.a
"""
我是module下面的__init__
>>>
"""
# 我們看到>>>只被打印了一次,證明沒有進行二次導入

所以通過module.a的方式來導入,即使沒有__init__.py文件依舊是可以訪問的,因為這是我在import的時候指定的。我們可以看一下sys.modules

import module.a
import sys

print(sys.modules["module"])
# <module 'module' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\__init__.py'>
print(sys.modules["module.a"])
# <module 'module.a' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\a.py'>

我們看到里面有一個模塊就叫做module.a,所以為什么通過import module.a的方式導入不需要__init__.py就很清晰了,因為我們並不是通過module來找a這個模塊,而是把a這個模塊我們通過import module.a的方式導入進來之后,就叫做module.a。但是為什么叫做module.a而不是a呢?因為如果還有一個包里面也有一個a的話,不就沖突了嗎?導入module.a和module1.a如果都叫做a的話,那么它們如何才能在sys.modules里面和平共存呢?另外,我們這里的包名叫module,是不是不太合適啊,應該叫做package更好一些,不過無所謂啦。

不過這里還有一個問題就是剛才說的,我們在導入module.a的時候,會先加載module,然后才會導入module.a,並且導入module.a的時候,還可以單獨使用module。畢竟這不是我們期望的結果,因為導入module.a的話,那么我們只是想使用module.a,不打算使用module,python為什么要這么做呢?事實上,這對python而言是必須的,根據我們對python虛擬機的執行原理的了解,python要想執行module.a,那么肯定要先從local空間找到module,然后才能找到a,如果不找到module的話,那么對a的查找也就無從談起。可我們剛才不是說module.a是一個整體嗎?事實上盡管是一個整體,但並不是說有一個模塊,這個模塊就叫做module.a。准確的說import module.a表示先導入module,然后再將module下面的a加入到module的屬性字典里面。我們說當module這個包里面沒有__init__.py的時候,那個這個包是無法使用的,因為屬性字典里面沒有相關屬性,但是當我們import module.a的時候,python會先導入module這個包,然后自動幫我們把a這個模塊加入到module這個包的屬性字典里面。

import module.a

# 此時的__init__里面啥也沒有
# 我們把__builtins__給pop掉,不然會輸出一大堆東西
module.__dict__.pop("__builtins__")
for name, m in module.__dict__.items():
    print(name, m)
"""
__name__ module
__doc__ None
__package__ module
...
...
...
a <module 'module.a' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\a.py'>
"""

我們看到__package__就是包名,如果是模塊那么就是None,然后我們看到了a。因此這種方式就跟我們定義了__init__文件、並在里面導入了a是一樣的。

假設module這個包里面有a和b兩個py文件,那么我們執行import module.aimport module.b會進行什么樣的動作應該就了如指掌了吧。執行import module.a,那么會先導入module,然后把a加到module的屬性字典里面,執行import module.b,還是會先導入包module,但是包module在上一步已經被導入了,所以此時直接會從sys.modules里面獲取,然后再把b加入到module的屬性字典里面。所以如果__init__.py里面有一個print的話,那么兩次導入顯然只會print一次,這種現象是由python對包中的模塊的動態加載機制決定的。還是那句話,一個包你就看成是里面的__init__.py文件即可,python對於包和模塊的區分不是特別明顯。

import module

# 有__init__文件
print(module.__file__)  # C:\Users\satori\Desktop\love_minami\module\__init__.py
print(module)  # <module 'module' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\__init__.py'>

import module

# 沒有__init__文件
print(module.__file__)  # None
print(module)  # <module 'module' (namespace)>

我們看到如果包里面有__init__.py文件,那么這個包的__file__屬性就是其內部的__init__.py文件,打印這個包,顯示的也是其內部的__init__.py模塊。如果沒有__init__.py文件,那么這個包的__file__就是一個None,打印這個包,顯示其是一個空間。另外,我們知道任何一個模塊(即使里面什么也不寫)的屬性字典里面都是有__builtins__屬性的,因為可以直接使用內置的對象、函數等等。而__init__.py也是屬於一個模塊,所以它也有__builtins__屬性的,由於一個包指向了內部的__init__.py,所以這個包的屬性字典也是有__builtins__屬性的。但如果這個包沒有__init__.py文件,那么這個包是沒有__builtins__屬性的。

import module

# 沒有__init__.py文件
print(module.__dict__.get("__builtins__"))  # None
import module

# 有__init__.py文件
print(module.__dict__.get("__builtins__")["int"])  # <class 'int'>

15.2.3.3 路徑搜索樹

假設我有這樣的一個目錄結構

那么python會將這個結構進行分解,得到一個類似於樹狀的節點集合

然后從左到右依次去sys.modules中查找每一個符號所對應的module是否已經被加載,如果一個包被加載了,比如說包module(所以說這個名字起得實在不合適)被加載了,那么在包module對應的PyModuleObject中會維護一個元信息__path__,表示這個package的路徑。比如我搜索A.a,當加載進來A的時候,那么a只會在A.__path__中進行,而不會在python的所有搜索路徑中執行了。

import module

# 打印module的__path__
print(module.__path__)  # ['C:\\Users\\satori\\Desktop\\love_minami\\module']

# 導入sys模塊
try:
    import module.sys
except ImportError as e:
    print(e)  # No module named 'module.sys'

# 顯然這樣是錯的,因為導入module.sys,那么就將搜索范圍只限定在module的__path__下面了

15.2.4 from與import

在python的import中,有一種精確控制所加載的對象的方法,通過from和import的結合,可以只將我們期望的module、甚至是module中的某個符號,動態地加載到內存中。這種機制使得python虛擬機在當前命名空間中引入的符號可以盡可能地少,從而更好地避免名字空間遭到污染。

按照我們之前所說,導入module下面的a模塊,我們可以使用import module.a的方式,但是此時a是在module的命名空間中,不是在我們當前模塊的命名空間中。也就是說我們希望能直接通過符號a來調用,而不是module.a,此時通過from ... import ...聯手就能完美解決。

from module import a
"""
>>>
"""
print(dir())
# [... '__loader__', '__name__', '__package__', '__spec__', 'a']

import sys
print(sys.modules.get("module"))
print(sys.modules.get("module.a"))
print(sys.modules.get("a"))  # None

首先通過dir()我們看到,確確實實將a這個符號加載到當前的命名空間里面了,但是在sys.modules里面卻沒有a。還是之前說的,a這個模塊是在module這個包里面的,你不可能不通過包就直接拿到包里面的模塊,因此在sys.modules里面的形式其實還是module.a這樣形式,只不過在當前模塊的命名空間中是a,a被映射到sys.modules["module.a"],另外我們看到除了module.a,module也導入進來了,這個原因我們之前也說過了,不再贅述。所以我們發現即便我們是from ... import ...,還是會觸發整個包的導入。只不過我們導入誰(假設從a導入b),就把誰加入到了當前模塊的命名空間里面(但是在sys.modules里面是沒有b的,而是a.b),並映射到sys.modules["a.b"]。

所以我們見識到了,即便是我們通過from module import a,還是會導入module這個包的,只不過module這個包是在sys.modules里面,並沒有暴露到local空間中,我們可以來證明這一點。

from module import a
"""
>>>
"""
import sys
print(id(sys.modules["module"]))  # 2015096408640
# 此時module盡管在sys.modules里面,但是卻沒有暴露在當前模塊的local空間里面
# 那么此時導入module,顯然會從sys.modules里面去找
import module
print(id(module))  # 2015096408640

完美證明我們之前的結論。

此外我們from module import a,導入的這個a是一個模塊,但是模塊a里面還有一個變量a,我們不加from,只通過import的話,那么最深也只能import到一個模塊,不可能說直接import模塊里面的某個變量、方法什么的。但是from ... import ...的話,確是可以的,比如我們from module.a import a,這句就表示我要導入module.a模塊里面變量a

from module.a import a

print(locals())  # {..., ..., 'a': 123}
# 我們看到此時module.a里面的a就進入了local空間里面的

import sys
modules = sys.modules
print("a" in modules)  # False
print("module.a" in modules)  # True
print("module" in modules)  # True

我們導入的a是一個變量,並不是模塊,所在sys.modules里面不會出現module.a.a這樣的東西存在,但是這個a畢竟是從module.a里面導入的,所以module.a是會在sys.modules里面的,同理module.a表示從module的屬性字典里面找a,所以module也是會進入sys.modules里面的。

最后還可以使用from module.a import *,這樣的機制把一個模塊里面所有的內容全部導入進來,本質和導入一個變量是一致的。但是在python中有一個特殊的機制,比如我們from p.m import *,如果m里面定義了__all__,那么只會導入__all__里面指定的屬性。

# module/a.py
__all__ = ["a", "b"]
a = 123
b = 345
c = 456
print(">>>")

我們注意到在__all__里面只指定了a和b,那么后續通過from module.a import *的時候,只會導入a和b,而不會導入c

from module.a import *
"""
>>>
"""
print("a" in locals() and "b" in locals())  # True
print("c" in locals())  # False

from module.a import c
print("c" in locals())  # True

我們注意到:通過import *導入的時候,是無法導入c的,因為c沒有在__all__中。但是即便如此,我們也可以通過單獨導入,把c導入進來。只是不推薦這么做,像pycharm這種智能編輯器也會提示:'c' is not declared in __all__。因為既然沒有在__all__里面,就證明這個變量是不希望被導入的,但是一般導入了也沒關系。

15.2.5 符號重命名

我們導入的時候一般為了解決符號沖突,往往會起別名,或者說符號重命名。比如包a和包b下面都有一個模塊叫做m,如果是from a import mfrom b import m的話,那么兩者就沖突了,后面的m會把上面的m覆蓋掉,不然python怎么知道要找哪一個m,所以這個時候我們會起別名,比如from a import m as m1from b import m as m2,不管是這種導入,import a as xximport a.a as xx也是支持的,但是from a import *是不支持as的。所以直接python都是將package、module或者變量自身的名字暴露給了local命名空間,而符號重命名則是python可以通過as關鍵字控制package、module、變量暴露給local命名空間的方式。

import module.a

print(module.a)
# <module 'module.a' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\a.py'>

print(module)
# <module 'module' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\__init__.py'>
import module.a as  A

print(A)
# <module 'module.a' from 'C:\\Users\\satori\\Desktop\\love_minami\\module\\a.py'>

import sys
print("module.a" in sys.modules)  # True
print("module" in sys.modules)  # True
print(module)
# NameError: name 'module' is not defined

看到結論我相信就應該心里有數了,不管我們有沒有as,既然import module.a,那么sys.modules里面就一定有module.a,和module。其實理論上有包module就夠了,但是我們說a是一個模塊,為了避免多次導入所以也要加到sys.modules里面,而且a又是module包里面,所以是module.a。而我們這里as A,那么A這個符號就暴露在了當前模塊的local空間里面,而且這個A就跟之前的module.a一樣,指向了module包下面的a模塊,無非是名字不同罷了。當然這不是重點,我們之前通過import module.a的時候,會自動把module也加入到當前模塊的local空間里面,也就是說通過import module.a是可以直接使用module的,但是當我們加上了as之后,發現module包已經不能訪問了。盡管都在sys.modules里面,但是對於加了as來說,此時的module這個包已經不在local命名空間里面了。一個as關鍵字,導致了兩者的不同,這是什么原因呢?我們后面分解。

15.2.6 符號的銷毀與重載

為了使用一個模塊,無論是內置的還是自己寫的,都需要import動態加載到內存,使用之后,我們也可能會刪除。刪除的原因一般是釋放內存啊等等。在python中,刪除一個對象可以使用del關鍵字,遇事不決del。

l = [1, 2, 3]
d = {"a": 1, "b": 2}

del l[0]
del d["a"]

print(l)  # [2, 3]
print(d)  # {'b': 2}


class A:

    def foo(self):
        pass


print("foo" in dir(A))  # True
del A.foo
print("foo" in dir(A))  # False

不光是列表、字典,好多東西del都能刪除,甚至是刪除某一個位置的值、或者方法。我們看到類的一個方法居然也能使用del刪除,但是對於module來說,del能做到嗎?顯然是可以做到的,或者更准確的說法是符號的銷毀符號關聯的對象的銷毀是一個概念嗎?python已經向我們隱藏了太多的動作,也采取了太多的緩存策略,當然對於python的使用者來說是好事情,因為把復雜的特性隱藏起來了,但是當我們想徹底的了解python的行為時,則必須要把這些隱藏的東西挖掘出來。

import module.a as A

# 對於模塊來說,dir()和locals()、globals()的keys是一致的
print("A" in dir())  # True
del A
print("A" in locals())  # False

import sys
print(id(sys.modules["module.a"]))  # 2985809163824

import module.a as 我不叫A了
print(id(我不叫A了))  # 2985809163824

我們看到在del之后,A這個符號確實從local空間消失了,或者說dir已經看不到了。但是后面我們發現,消失的僅僅是A這個符號,至於module.a這個PyModuleObject依舊在sys.modules里面巋然不動。然而,盡管它還存在於python系統中,但是我們的程序再也無法感知到,但它就在那里不離不棄。所以此時python就成功地向我們隱藏了這一切,我們的程序認為:module.a已經不存在了

不過為什么python要采用這種看上去類似模塊池的緩存機制呢?因為組成一個完整系統的多個py文件可能都要對某個module進行import動作。所以要是從sys.modules里面刪除了,那么就意味着需要重新從文件里面讀取,如果不刪除,那么只需要從sys.modules里面暴露給當前的local命名空間即可。所以import實際上並不等同我們所說的動態加載,它的真實含義是希望某個模塊被感知,也就是將這個模塊以某個符號的形式引入到某個命名空間。這些都是同一個模塊,如果import等同於動態加載,那么python對同一個模塊執行多次動態加載,並且內存中保存一個模塊的多個鏡像,這顯然是非常愚蠢的。

所以python引入了全局的module集合--sys.modules,這個集合作為模塊池,保存了模塊的唯一值。當某個模塊通過import聲明希望感知到某個module時,python將在這個池子里面查找,如果被導入的模塊已經存在於池子中,那么就引入一個符號到當前模塊的命名空間中,並將其關聯到導入的模塊,使得被導入的模塊可以透過這個符號被當前模塊(都是執行import導入的模塊)感知到。而如果被導入的模塊不在池子里,python這才執行動態加載的動作。

如果這樣的話,難道一個模塊在被加載之后,就不能改變了。假如在加載了模塊a的時候,如果我們修改了模塊a,難道python程序只能先暫停再重啟嗎?顯然不是這樣的,python的動態特性不止於此,它提供了一種重新加載的機制,使用importlib模塊,通過importlib.reload(module),可以實現重新加載並且這個函數是有返回值的,返回加載之后的模塊。

首先我們的a模塊里面啥也沒有,所以dir(a)只是顯示幾個帶有雙下划線的屬性,但是我們在a.py里面增加了name和age變量,然后重新加載模塊,dir(a)顯示多個name和age兩個屬性。然后我們在a.py中刪除age屬性、增加一個gender屬性,再重新加載,dir(a)查看,首先gender屬性確實加進來了了,但是我們發現age還在里面。理論上age屬性應該從dir(a)里面消失才對啊,那這個age它又能不能調用呢?

並且我們看到,此時我們依然可以通過a來調用age屬性。那么根據這個現象我們是不是可以大膽猜測,python在reload一個模塊的時候,只是將模塊里面新的符號加載進來,而刪除的則不管了,那么這個猜測到底正不正確呢,別急我們后續揭曉。我們下面先通過源碼來剖析一下import的實現機制。

15.3 import機制的實現

從前面的黑盒探測我們已經對import機制有了一個非常清晰的認識,python的import機制基本上可以切分為三個不同的功能。

  • python運行時的全局模塊池的維護和搜索
  • 解析與搜索模塊路徑的樹狀結構
  • 對不同文件格式的模塊的動態加載機制

盡管import的表現形式千變萬化,但是都可以歸結為:import x.y.z的形式。因為import sys也可以看成是x.y.z的一種特殊形式。而諸如from、as與import的結合,實際上同樣會進行import x.y.z的動作,只是最后在當前命名空間中引入符號時各有不同。所以我們就以import x.y.z的形式來進行分析。

我們說導入模塊,是調用__import__,那么我們就來看看這個函數長什么樣子

static PyObject *
builtin___import__(PyObject *self, PyObject *args, PyObject *kwds)
{
    static char *kwlist[] = {"name", "globals", "locals", "fromlist",
                             "level", 0};
    //初始化globals、fromlist都為NULL,
    PyObject *name, *globals = NULL, *locals = NULL, *fromlist = NULL;
    //表示默認絕對導入
    int level = 0;
	
    //從tuple中解析出需要的信息
    if (!PyArg_ParseTupleAndKeywords(args, kwds, "U|OOOi:__import__",
                    kwlist, &name, &globals, &locals, &fromlist, &level))
        return NULL;
    //導入模塊
    return PyImport_ImportModuleLevelObject(name, globals, locals,
                                            fromlist, level);
}

另外,PyArg_ParseTupleAndKeywords這個函數在python中是一個被廣泛使用的函數,原型如下:

//Python/getargs.c
int PyArg_ParseTupleAndKeywords(PyObject *, PyObject *,
                                const char *, char **, ...);

這個函數的目的是將args和kwds中所包含的所有對象按format中指定的格式解析成各種目標對象,可以是python中的對象(PyListObject、PyLongObject等等),也可以是C的原生對象。

我們知道這個args實際上是一個PyTupleObject對象,包含了__import__函數運行所需要的參數和信息,它是python虛擬機在執行IMPORT_NAME的時候打包而產生的,然而在這里,python虛擬機進行了一個逆動作,將打包后的這個PyTupleObject拆開,重新獲得當初的參數。python在自身的實現中大量的使用了這樣打包、拆包的策略,使得可變數量的對象能夠很容易地在函數之間傳遞。

在解析參數的過程中,指定解析格式的format中可用的格式字符有很多,這里只看一下__import__用到的格式字符。其中s代表目標對象是一個char *,通常用來將tuple中的PyUnicodeObject對象解析成char *,i則用來將tuple中的PyLongObject解析成int,而O則代表解析的目標對象依然是一個python中的合法對象,通常這表示PyArg_ParseTupleAndKeywords不進行任何的解析和轉換,因為在PyTupleObject對象中存放的肯定是一個python的合法對象。至於|和:,它們不是非格式字符,而是指示字符,|指示其后所帶的格式字符是可選的。也就是說,如果args中只有一個對象,那么__import__PyArg_ParseTupleAndKeywords的調用也不會失敗。其中,args中的那個對象會按照s的指示被解析為char *,而剩下的global、local、fromlist則會按照O的指示被初始化為Py_None,level是0。而:則指示"格式字符"到此結束了,其后所帶字符串用於在解析過程中出錯時,定位錯誤的位置所使用的。

在完成了對參數的拆包動作之后,然后進入了PyImport_ImportModuleLevelObject,這個我們在import_name中已經看到了,而且它也是先獲取__builtin__里面的__import__函數指針。

PyObject *
PyImport_ImportModuleLevelObject(PyObject *name, PyObject *globals,
                                 PyObject *locals, PyObject *fromlist,
                                 int level)
{
    _Py_IDENTIFIER(_handle_fromlist);
    PyObject *abs_name = NULL;
    PyObject *final_mod = NULL;
    PyObject *mod = NULL;
    PyObject *package = NULL;
    PyInterpreterState *interp = PyThreadState_GET()->interp;
    int has_from;
	
    //name為空直接報錯
    if (name == NULL) {
        PyErr_SetString(PyExc_ValueError, "Empty module name");
        goto error;
    }
    //那么必須是字符串
    if (!PyUnicode_Check(name)) {
        PyErr_SetString(PyExc_TypeError, "module name must be a string");
        goto error;
    }
    if (PyUnicode_READY(name) < 0) {
        goto error;
    }
    //level必須大於等0
    if (level < 0) {
        PyErr_SetString(PyExc_ValueError, "level must be >= 0");
        goto error;
    }
    //name大於0,獲取父級目錄
    if (level > 0) {
        abs_name = resolve_name(name, globals, level);
        if (abs_name == NULL)
            goto error;
    }
    //否則name是其本身
    else {  /* level == 0 */
        if (PyUnicode_GET_LENGTH(name) == 0) {
            PyErr_SetString(PyExc_ValueError, "Empty module name");
            goto error;
        }
        abs_name = name;
        Py_INCREF(abs_name);
    }
	
    //傳入abs_name,獲取模塊,這里調用了PyImport_GetModule
    mod = PyImport_GetModule(abs_name);
    if (mod == NULL && PyErr_Occurred()) {
        goto error;
    }
    //如果這個mod不是NULL也不是Py_None
    if (mod != NULL && mod != Py_None) {
        _Py_IDENTIFIER(__spec__);
        _Py_IDENTIFIER(_initializing);
        _Py_IDENTIFIER(_lock_unlock_module);
        PyObject *value = NULL;
        PyObject *spec;
        int initializing = 0;

        /* Optimization: only call _bootstrap._lock_unlock_module() if
           __spec__._initializing is true.
           NOTE: because of this, initializing must be set *before*
           stuffing the new module in sys.modules.
         */
        //設置模塊的__spec__屬性,這個__spec__記錄模塊的詳細信息,是一個ModuleSpec對象
        spec = _PyObject_GetAttrId(mod, &PyId___spec__);
        if (spec != NULL) {
            value = _PyObject_GetAttrId(spec, &PyId__initializing);
            Py_DECREF(spec);
        }
        if (value == NULL)
            PyErr_Clear();
        else {
            initializing = PyObject_IsTrue(value);
            Py_DECREF(value);
            if (initializing == -1)
                PyErr_Clear();
            if (initializing > 0) {
                //這里是要拿到鎖
                //python虛擬機在import之前,會對import這個動作上鎖。
                //目的就是為了保證多個線程對同一個模塊時進行import時,不會出岔子
                //如果沒有這個同步鎖,那么可能會產生一些異常現象
                //當然在import結束之后,還是開鎖
                value = _PyObject_CallMethodIdObjArgs(interp->importlib,
                                                &PyId__lock_unlock_module, abs_name,
                                                NULL);
                if (value == NULL)
                    goto error;
                Py_DECREF(value);
            }
        }
    }
    else {
        Py_XDECREF(mod);
        mod = import_find_and_load(abs_name);
        if (mod == NULL) {
            goto error;
        }
    }

    has_from = 0;
    //fromlist是一個tuple
    if (fromlist != NULL && fromlist != Py_None) {
        has_from = PyObject_IsTrue(fromlist);
        if (has_from < 0)
            goto error;
    }
    if (!has_from) {
        //在package的__init__中進行import動作
        Py_ssize_t len = PyUnicode_GET_LENGTH(name);
        if (level == 0 || len > 0) {
            Py_ssize_t dot;

            dot = PyUnicode_FindChar(name, '.', 0, len, 1);
            if (dot == -2) {
                goto error;
            }

            if (dot == -1) {
                /* No dot in module name, simple exit */
                final_mod = mod;
                Py_INCREF(mod);
                goto error;
            }

            if (level == 0) {
                PyObject *front = PyUnicode_Substring(name, 0, dot);
                if (front == NULL) {
                    goto error;
                }

                final_mod = PyImport_ImportModuleLevelObject(front, NULL, NULL, NULL, 0);
                Py_DECREF(front);
            }
            else {
                Py_ssize_t cut_off = len - dot;
                Py_ssize_t abs_name_len = PyUnicode_GET_LENGTH(abs_name);
                PyObject *to_return = PyUnicode_Substring(abs_name, 0,
                                                        abs_name_len - cut_off);
                if (to_return == NULL) {
                    goto error;
                }

                final_mod = PyImport_GetModule(to_return);
                Py_DECREF(to_return);
                if (final_mod == NULL) {
                    if (!PyErr_Occurred()) {
                        PyErr_Format(PyExc_KeyError,
                                     "%R not in sys.modules as expected",
                                     to_return);
                    }
                    goto error;
                }
            }
        }
        else {
            final_mod = mod;
            Py_INCREF(mod);
        }
    }
    else {
        final_mod = _PyObject_CallMethodIdObjArgs(interp->importlib,
                                                  &PyId__handle_fromlist, mod,
                                                  fromlist, interp->import_func,
                                                  NULL);
    }

  error:
    Py_XDECREF(abs_name);
    Py_XDECREF(mod);
    Py_XDECREF(package);
    if (final_mod == NULL)
        remove_importlib_frames();
    return final_mod;
}

其實每一個包和模塊都有一個__name____path__屬性

import numpy as np
import numpy.core
import six

print(np.__name__, np.__path__)  # numpy ['C:\\python37\\lib\\site-packages\\numpy']
print(np.core.__name__, np.core.__path__)  # numpy.core ['C:\\python37\\lib\\site-packages\\numpy\\core']
print(six.__name__, six.__path__)  # six []

__name__就是模塊或者包名,如果包下面的包或者模塊,那么就是包名.包名或者包名.模塊名,至於__path__則是包所在的路徑,但是這個和__file__又是不一樣的,如果是__file__則是指向內部的__init__.py文件,沒有則為None。但是對於模塊來說,則沒有__path__

精力有限,具體的不再深入。我們下面從python的角度來理解一下吧

15.4 python中的import操作

15.4.1 import module

import sys 
"""
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sys)
              6 STORE_NAME               0 (sys)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE
"""

這是我們一開始考察的例子,現在我們已經很清楚地了解了IMPORT_NAME的行為,在IMPORT_NAME指令的最后,python虛擬機會將PyModuleObject對象壓入到運行時棧內,隨后會將(sys, PyModuleObject)存放到當前的local命名空間中。

15.4.2 import package

import sklearn.linear_model.ridge
"""
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sklearn.linear_model.ridge)
              6 STORE_NAME               1 (sklearn)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE
"""

如果涉及的是對package的import動作,那么IMPORT_NAME的指令參數則是關於module的完整路徑信息,IMPORT_NAME指令的內部將解析這個路徑,並為sklearnsklearn.linear_modelsklearn.linear_model.ridge都創建一個PyModuleObject對象,這三者都存在於sys.modules里面。但是我們看到STORE_NAME是sklearn,表示只有sklearn對應的PyModuleObject放在了當前模塊的local空間里面,可為什么是sklearn呢?難道不應該是sklearn.linear_model.ridge嗎?其實經過我們之前的分析這一點已經不再是問題了,因為import sklearn.linear_model.ridge並不是說導入一個模塊或包叫做sklearn.linear_model.ridge,而是先導入sklearn,然后把linear_model放在sklearn的屬性字典里面,把ridge放在linear_model的屬性字典里面。同理sklearn.linear_model.ridge代表的是先從local空間里面找到sklearn,再從sklearn的屬性字典中找到linear_model,然后在linear_model的屬性字典里面找到ridge。而我們說,linear_modelridge已經在對應的包的屬性字典里面的了,我們通過sklearn一級一級往下找是可以找到的,因此只需要把skearn返回即可,或者說返回sklearn.linear_model.ridge本身就是不合理的,因為這表示導入一個名字就叫做sklearn.linear_model.ridge的模塊或者包,但顯然不存在,即便我們創建了,但是由於python的語法解析規范依舊不會得到想要的結果。不然的話,假設import module.a,那python是導入名為module.a的模塊或包呢?還是導入module下的a呢?

也正如我們之前分析的module.a,我們import module.a的時候,會把module加載進來,然后把a加到module的屬性字典里面,然后只需要把module返回即可,因為我們通過module是可以找到a,而且也不存在我們期望的module.a,因為這個module.a代表的含義是從module的屬性字典里面獲取a,所以import module.a是必須要返回module的,而且只返回了module。至於sys.modules(一個字典)里面是存在字符串名為module.a的key的,這是為了避免重復導入所采取的策略,但它依舊表示從module里面獲取a。

import pandas.core

print(pandas.DataFrame({"a": [1, 2, 3]}))
"""
   a
0  1
1  2
2  3
"""
# 導入pandas.core會先執行pandas的__init__文件
# 所有通過pandas.DataFrame是可以調用的

15.4.3 from & import

from sklearn.linear_model import ridge
"""
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (('ridge',))
              4 IMPORT_NAME              0 (sklearn.linear_model)
              6 IMPORT_FROM              1 (ridge)
              8 STORE_NAME               1 (ridge)
             10 POP_TOP
             12 LOAD_CONST               2 (None)
             14 RETURN_VALUE
"""

注意此時的LOAD_CONST 1不再是None了,而是一個tuple。此時python是將ridge放到了當前模塊的local空間中,並且sklearn、sklearn.linear_model都被導入了,並且存在於sys.modules里面,但是sklearn卻並不在當前local空間中,盡管這個對象被創建了,但是它被python隱藏了。IMPORY_NAME是sklearn.linear_model,也表示導入sklearn,然后把sklearn下面的linear_model加入到sklearn的屬性字典里面。其實sklearn沒在local空間里面,還可以這樣理解。只有import的時候,那么我們必須從頭開始一級一級向下調用,所以頂層的包必須加入到local空間里面,但是from sklearn.linear_model import ridge是把ridge導出,此時ridge已經指向了sklearn下面的linear_model下面的ridge,那么此時就不需要sklearn了,或者說sklearn就沒必要暴露在local空間里面了。並且sys.modules里面也不存在ridge這個key,存在的是sklearn.linear_model.ridge,暴露給當前模塊的local空間里面的符號是ridge

15.4.4 import & as

import sklearn.linear_model.ridge as xxx
"""
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (sklearn.linear_model.ridge)
              6 IMPORT_FROM              1 (linear_model)
              8 ROT_TWO
             10 POP_TOP
             12 IMPORT_FROM              2 (ridge)
             14 STORE_NAME               3 (xxx)
             16 POP_TOP
             18 LOAD_CONST               1 (None)
             20 RETURN_VALUE

"""

這個和帶有from的import類似,sklearnsklearn.linear_modelsklearn.linear_model.ridge都在sys.modules里面,但是我們加上了as xxx,那么這個xxx就直接指向了sklearn下面的linear_model下面的ridge,就不需要sklearn了。這個和上面的from & import類似,只有xxx暴露在了當前模塊的local空間里面,sklearn雖然在sys.modules里面,但是在當前模塊就無法訪問了。

15.5.5 from & import & as

from sklearn.linear_model import ridge as xxx

這個我想連字節碼都不需要貼了,和之前from & import一樣,只是最后暴露給當前模塊的local空間的ridge變成了我們自己指定的xxx

15.5 與module有關的命名空間問題

同函數、類一樣,每個PyModuleObject也是有自己的命名空間的,舉個例子。

# module/a.py
name = "hanser"


def print_name():
    print(name)
    
    
# b.py, b和module同級
from module.a import name, print_name
name = "yousa"  # 將name改為"yousa"
print_name()

執行b.py,會發現打印的依舊是"hanser",我們說python是根據LEGB規則,而print_name里面沒有name,那么去外層找,b.py里面的name是"yousa",但是找到的依舊是a.py里面的"hanser"。為什么?

其實用我們之前的結論依舊可以解釋的通,from module.a import name, print_name,這個print_name指向的是a.py里面的print_name,盡管它被導入到了b.py中,但是它記得自己從何處來,所以print(name)的時候,打印的依舊是a.py里面name。可以認為導入的模塊,不管是import、還是from&import,導入過來的本身自帶一層作用域,所以這個print_name是在a.py里面,盡管它是在b.py里面被執行的。如果a.py里面沒有name呢?那么不好意思會報錯,不會到b.py里面去找name,因為作用按照LEGB規則,但是它是沒有辦法跨模塊的。不可能說,a.py里面LEGB找不到name,被導入到b.py里面之后,就會從b.py的LEGB去找name,這是不存在的。每個模塊都有自己的命名空間(共享__builtins__),屬性的查找是無法跨越模塊的。

當a.py里面的屬性print_name被導入到b.py中,盡管它是在b里面,但是你可以看做這個print_name四周有一堵牆,這個牆就是a.py施加的,導致print_name查找屬性只能在a.py里面查找,因為它指向的就是a.py,是不可能跨越這堵牆到b.py里面查找的。而from module.a import name, print_name就等價於先導入module,然后把a放到module的屬性字典里面,然后由於我們import的是模塊里面的內容,所以會把a.py里面的內容拿到b.py里面執行一遍,然后把name和print_name暴露給當前模塊(也就是b.py)的local空間中,這都沒問題。但是,如果在a.py中存在絕對導入的import,那么此時import所在的路徑就不再是a.py了,而b.py所在路徑。盡管name、print_name等屬性的查找還是會從a.py中查找,但是對於絕對導入的模塊,則變成了從b.py中導入,因為它被拿到b.py里面執行了嘛。

所以說,命名空間是python的靈魂。

15.6 完


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM