楔子
上一篇我們介紹了生成器,本來這里應該介紹協程的,但是大致閱讀了一下,感覺如果從源碼的角度來介紹協程的話,工作量太大。而且個人精力有限,所以推薦我寫的這一篇博客:https://www.cnblogs.com/traditional/p/11828780.html
,是用來介紹asyncio的,當然也從Python的角度介紹了Python中的協程。
這一次我們說一下Python模塊的加載機制,我們之前所考察的所有內容都具有一個相同的特征,那就是它們都局限在一個py文件中。然而現實中不可能只有一個py文件,而是存在多個,而多個py文件之間存在引用和交互,這些也是程序的一個重要組成部分。那么這里我們就來分析,Python中模塊的導入機制。
在這里我們必須強調一點,Python中一個單獨的py文件、或者pyd文件,我們稱之為一個 模塊 ;而多個模塊組合起來放在一個目錄中,這個目錄我們稱之為 包 。
但是不管是模塊,還是包,它們在Python的底層都是PyModuleObject結構體實例,類型為PyModule_Type,而在Python中則都是一個<class 'module'>
對象。
//Objects/moduleobject.c
PyTypeObject PyModule_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"module", /* tp_name */
sizeof(PyModuleObject), /* tp_basicsize */
//...
};
//Python中的<class 'module'>對應底層的PyModule_Type
//而導入進來的模塊對象 則對應底層的 PyModuleObject
所以模塊和包導入進來之后也是一個對象,下面我們通過Python來演示一下。
import os
import pandas
print(os) # <module 'os' from 'C:\\python38\\lib\\os.py'>
print(pandas) # <module 'pandas' from 'C:\\python38\\lib\\site-packages\\pandas\\__init__.py'>
print(type(os)) # <class 'module'>
print(type(pandas)) # <class 'module'>
因此不管是模塊還是包,在Python中都是一樣的,我們后面會詳細說。總之它們都是一個PyModuleObject,只不過為了區分,我們把單獨的py文件、pyd文件叫做模塊,一個目錄叫做包,但是在Python的底層則並沒有區分那么明顯,它們都是一樣的。
所以為了不產生歧義,我們這里做一個約定,從現在開始本系列中出現的"模塊":指的就是單獨的可導入文件;出現的"包":指的就是目錄;而"模塊"和"包"組合起來,我們稱之為module對象,因為這兩者本來就是<class 'module'>
的實例對象。
import前奏曲
我們以一個簡單的import為序幕,看看相應的字節碼;
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存儲在當前的local名字空間中,然后當我們調用sys.path的時候,虛擬機就能很輕松地通過sys來獲取path這個屬性所對應的值了。因此就像我們之前說的那樣,創建函數、類、導入模塊等等,它們本質上和通過賦值語句創建一個變量是沒有什么區別的,關鍵就是這個IMPORT_NAME,我們看看它的實現,知道從哪里看嗎?我們說Python中所有指令集的實現都在 ceval.c 的那個無限for循環的巨型switch中。
case TARGET(IMPORT_NAME): {
//PyUnicodeObject對象,比如import pandas,那么這個name就是字符串pandas
PyObject *name = GETITEM(names, oparg);
//我們看到這里有一個fromlist和level,顯然需要從運行時棧中獲取對應的值,那么顯然要先將值壓入運行時棧
//我們再看一下剛才的字節碼,我們發現在IMPORT_NAME之前有兩個LOAD_CONST,將0和None壓入了運行時棧
//因此這里會從運行時棧中獲取到None和0,然后分別賦值給fromlist和level,至於這兩個是干啥的,我們后面說
PyObject *fromlist = POP();
PyObject *level = TOP();
PyObject *res; //一個PyModuleObject *,也就是模塊對象
//調用import_name, 將該函數的返回值賦值給res
res = import_name(tstate, f, name, fromlist, level);
Py_DECREF(level);
Py_DECREF(fromlist);
SET_TOP(res);
if (res == NULL)
goto error;
DISPATCH();
}
因此重點在import_name這個函數中,但是在此之前我們需要重點關注一下這個fromlist和level,而這一點我們可以從Python的層面來介紹。我們知道在python中,我們導入一個模塊直接通過import關鍵字即可, 但是除了import,我們還可以使用__import__函數來進行導入,這個__import__是解釋器使用的一個函數,不推薦我們直接使用,但是我想說的是import os就等價於os = __import__("os")。
os = __import__("os")
SYS = __import__("sys")
print(os) # <module 'os' from 'C:\\python38\\lib\\os.py'>
print(SYS.prefix) # C:\python38
但是問題來了:
m1 = __import__("os.path")
print(m1) # <module 'os' from 'C:\\python38\\lib\\os.py'>
# 我們驚奇地發現,居然還是os模塊,按理說應該是os.path(windows系統對應ntpath)啊
m2 = __import__("os.path", fromlist=[""])
print(m2) # <module 'ntpath' from 'C:\\python38\\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.py(也可能是os.pyd、或者一個名為os的目錄也可以)得到一個module對象,並將返回值再次賦值給符號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)
# 很方便的就導入了, 字節通過字符串的方式導入一個module對象
print(pd)
"""
<module 'pandas' from 'C:\\python38\\lib\\site-packages\\pandas\\__init__.py'>
"""
# 如果想導入一個"模塊中導入的另一個模塊", 比如: 模塊a中導入了模塊b, 我們希望導入a.b
# 或者導入一個包下面的子模塊等等, 比如: pandas.core.frame
sub_mod = importlib.import_module("pandas.core.frame")
# 我們看到可以自動導入pandas.core.frame
print(sub_mod)
"""
<module 'pandas.core.frame' from 'C:\\python38\\lib\\site-packages\\pandas\\core\\frame.py'>
"""
# 但如果是__import__, 默認的話是不行的, 導入的依舊是最外層pandas
print(__import__("pandas.core.frame"))
"""
<module 'pandas' from 'C:\\python38\\lib\\site-packages\\pandas\\__init__.py'>
"""
# 可以通過給fromlist指定一個非空列表來實現
print(__import__("pandas.core.frame", fromlist=[""]))
"""
<module 'pandas.core.frame' from 'C:\\python38\\lib\\site-packages\\pandas\\core\\frame.py'>
"""
扯了這么多,我們來看看之前源碼中說的import_name。
//ceval.c
case TARGET(IMPORT_NAME): {
// 這個函數接收了五個參數,tstate:線程狀態對象、f:棧幀、name:模塊名、fromlist:一個None、level:0
res = import_name(tstate, f, name, fromlist, level);
}
static PyObject *
import_name(PyThreadState *tstate, PyFrameObject *f,
PyObject *name, PyObject *fromlist, PyObject *level)
{
_Py_IDENTIFIER(__import__);
PyObject *import_func, *res;
PyObject* stack[5];
//獲取內建函數__import__
import_func = _PyDict_GetItemIdWithError(f->f_builtins, &PyId___import__);
//為NULL獲取失敗, 顯然這些都是Python底層做的檢測, 在Python使用上不會出現
//如果出現, 只能說明解釋器出問題了
if (import_func == NULL) {
if (!_PyErr_Occurred(tstate)) {
_PyErr_SetString(tstate, PyExc_ImportError, "__import__ not found");
}
return NULL;
}
//判斷__import__是否被重載了
if (import_func == tstate->interp->import_func) {
int ilevel = _PyLong_AsInt(level);
if (ilevel == -1 && _PyErr_Occurred(tstate)) {
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;
}
然后我們看到底層又調用了 PyImport_ImportModuleLevelObject ,我們來看一下它的實現。
//Python/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 = _PyInterpreterState_GET_UNSAFE();
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;
}
else { //否則的話,說明level==0,因為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獲取module對象
//注意:這個module對象會從sys.modules里面獲取,並不會重新加載
//我們說在Python中,導入一個module對象的時候會從sys.modules里面查找
//如果沒有,那么才從硬盤上加載。一旦加載,那么會直接設置在sys.modules里面
//在下一次導入的時候,直接從sys.modules中獲取,具體細節后面聊
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;
}
另外關於module對象的導入方式,Python也提供了非常豐富的寫法,比如:
import numpy
import numpy as np
import numpy.random as _random
from numpy import random
from numpy import random as _random
from numpy import *
從import的目標來說,可以是"包",也可以是模塊。而模塊可以通過py文件作為載體,也可以通過dll(pyd)
或者so等二進制文件作為載體,下面我們就來一一介紹。
import機制的黑盒探測
同C++的namespace,Python通過模塊和包來實現對系統復雜度的分解,以及保護名字空間不受污染。通過模塊和包,我們可以將某個功能、某種抽象進行獨立的實現和維護,在module對象的基礎之上構建軟件,這樣不僅使得軟件的架構清晰,而且也能很好的實現代碼復用。
標准import
Python內建module
sys這個模塊恐怕是使用最頻繁的module對象之一了,我們就從這位老鐵入手。Python中有一個內置函數dir,這個小工具是我們探測import的殺手鐧。如果你在交互式環境下輸入dir(),那么會打印當前local名字空間的所有符號,如果有參數,則將參數視為對象,輸出該對象的所有屬性。我們先來看看import動作對當前名字空間的影響:
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__']
>>>
>>> import sys
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'sys']
>>>
我們看到當我們進行了import動作之后,當前的local名字空間增加了一個sys符號。而且通過type操作,我們看到這個sys符號指向一個module對象,我們說在底層它是一個PyModuleObject。當然雖然寫着類型是<class 'module'>
,但是我們在Python中是無法直接使用module這個類的。不過它既然是一個class,那么就一定繼承object,並且元類為type。
>>> sys.__class__.__class__
<class 'type'>
>>> sys.__class__.__base__
<class 'object'>
>>>
這與我們的分析是一致的。言歸正傳,我們看到import機制影響了當前local名字空間,使得加載的module對象在local空間成為可見的。實際上,這和我們創建一個變量的時候,也會影響local名字空間。引用該module的方法正是通過module的名字,即這里的sys。
不過這里還有一個問題,我們來看一下:
>>> sys
<module 'sys' (built-in)>
>>>
我們看到sys是內置的,說明模塊除了真實存在文件之外,還可以內嵌在解釋器里面。但既然如此,那為什么我們不能直接使用,還需要導入呢?其實不光是sys,在Python初始化的時候,就已經將一大批的module對象加載到了內存中。但是為了使得當前local名字空間能夠達到最干凈的效果,Python並沒有將這些符號暴露在local名字空間中,而是需要用戶顯式的使用import機制來將這個符號引入到local名字空間中,才能讓程序使用這個符號背后的對象。
我們知道,凡是加載進內存的module對象都保存在sys.modules里面,盡管當前的local空間里面沒有,但是sys.modules里面是跑不掉的。
import sys
# 這個modules是一個字典,里面分別為 module對象的名字 和 對應的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
一開始這些module對象是不在local空間里面的,除非我們顯式導入,但是即便我們導入,這些module對象也不會被二次加載,因為已經在初始化的時候就被加載到內存里面了。因此對於已經在sys.modules里面的module對象來說,導入的時候只是加到local空間里面去,所以代碼中的os和os_的id是一樣的。如果我們在Python啟動之后,導入一個sys.modules中不存在的module對象,那么才會進行加載、然后同時進入local空間和sys.modules。
用戶自定義module
我們知道,對於那些內嵌在解釋器里面的module對象,如果import,只是將該module對象暴露在了local名字空間中。下面我們看看對於那些沒有在初始化的時候加載到內存的module對象進行import的時候,會出現什么樣動作。這里就以模塊為例,當然正如我們之前說的,一個模塊的載體可以是py文件或者二進制文件,py文件可以是自己編寫的、也可以是標准庫中的、或者第三方庫中的。不過我們目前不區分那么多,通過自己編寫的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名字空間中,而且這個被動態加載的模塊也在sys.module中擁有了一席之地,而且它們背后隱藏的是同一個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__) # D:\satori\a.py
這里可以看到,module對象內部實際上是通過一個dict在維護所有的module對象{名字: 屬性值}
,里面有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對象,一個是字典。我們通過__builtins__
獲取的是一個module對象,里面存放了int、str、globals等內建對象和內建函數等等,我們直接輸入int、str、globals和通過__builtins__.int
,__builtins__.str
,__builtins__.globals
的效果是一樣的,我們輸入__builtins__
可以拿到這個內置模塊,通過這個內置模塊去獲取里面的內容,當然也可以直接獲取里面的內容,因為這些已經是全局的了。
但是a.__dict__["__builtins__"]
是一個字典,這就說明兩個從性質上就是不同的東西,但即便如此,就真的一點關系也沒有嗎?
import a
print(id(__builtins__.__dict__)) # 2791398177216
print(id(a.__dict__["__builtins__"])) # 2791398177216
我們看到還是有一點關系的,和類、類的實例對象一樣,每一個module對象也有自己的屬性字典__dict__
,記錄了自身的元信息、里面存放的內容等等。對於a.__dict__["__builtins__"]
來說,拿到的就是__builtins__.__dict__
,所以說__builtins__
是一個模塊,但是這個模塊有一個__dict__
屬性字典,而這個字典是可以通過module對象.__dict__["__builtins__"]
來獲取的,因為任何一個模塊都可以使用__builtins__
里面的內容,並且所有模塊對應的__builtins__
都是一樣的。所以當你直接打印a.__dict__
的時候會輸出一大堆內容,因為輸出的內容里面不僅有當前模塊的內容,還有__builtins__.__dict__
。
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__
所以可以把模塊的屬性字典,看成是local空間、內置空間的組合。
嵌套import
我們下面來看一下import的嵌套,所謂import的嵌套就是指,假設我import a,但是在a中又import b,我們來看看這個時候會發生什么有趣的動作。
# a.py
import sys
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里面做了什么是看不到的。
import a
import sys
print(a.sys is sys is sys.modules["sys"] is a.__dict__["sys"] is a.sys) # True
首先我們import a,那么a模塊就在當前模塊的屬性字典里面了,我們通過a這個符號是可以直接拿到其對應的模塊的。但是在a中我們又import sys,那么這個sys模塊就已經在a模塊對應的屬性字典里面了,也就是說,我們這里通過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
"""
123
"""
import b
"""
123
"""
import a
import b
import c
"""
123
"""
當導入一個不在sys.modules的模塊時,會先從硬盤中加載相應的文件,然后逐行解釋執行里面的內容,構建PyModuleObject對象,加入到sys.modules里面。當第二次導入的時候,直接將符號暴露在當前的local空間中,就不會再執行里面的內容了。
所以我們可以把sys.modules看成是一個大倉庫,任何導入了的模塊都在這里面。如果再導入的話,在sys.modules里面找到了,就直接返回即可,這樣可以避免重復加載。
導入包
我們寫的多個邏輯或者功能上相關的函數、類可以放在一個模塊里面,那么多個模塊是不是也可以組成一個包呢?如果說模塊是管理class、函數、一些變量的機制,那么包就是管理模塊的機制,當然啦,多個小的包又可以聚合成一個較大的包。
因此在Python中,模塊是由一個單獨的文件來實現的,可以是py文件、或者二進制文件。而對於包來說,則是一個目錄,里面容納了模塊對應的文件,這種方式就是把多個模塊聚合成一個包的具體實現。
現在我有一個名為test_import的模塊,里面有一個a.py:
a.py內容如下
a = 123
b = 456
print(">>>")
現在我們來導入它
import test_import
print(test_import) # <module 'test_import' (namespace)>
在Python2中,這樣是沒辦法導入的,因為如果一個目錄要成為Python中的包,那么里面必須要有一個__init__
文件,但是在Python3中沒有此要求。而且我們發現print之后,顯示的也是一個module對象,因此Python對於模塊和包的底層定義其實是很靈活的,並沒有那么僵硬。
import test_import
print(test_import.a)
"""
AttributeError: module 'test_import' has no attribute 'a'
"""
然而此時神奇的地方出現了,我們調用test_import.a的時候,告訴我們沒有a這個屬性。很奇怪,我們的test_import里面不是有a.py嗎?首先python導入一個包,會先執行這個包的__init__
文件,只有在__init___
文件中導入了,我們才可以通過包名來調用。如果這個包里面沒有__init__
文件,那么你導入這個包,是什么屬性也用不了的。光說可能比較難理解,我們來演示一下。我們先來創建__init__
文件,但是里面什么也不寫。
import test_import
print(test_import) # <module 'test_import' from 'D:\\satori\\test_import\\__init__.py'>
此時我們又看到了神奇的地方,我們在test_import目錄里面創建了__init__
文件之后,再打印test_import,得到結果又變了,告訴我們這個包來自於該包里面的__init__
文件。所以就像我們之前說的,Python對於包和模塊的概念區分的不是很明顯,我們把包就當做該包下面的__init__
文件即可,這個__init__
中定義了什么,那么這個包里面就有什么。
# test_import/__init__.py
import sys
from . import a
name = "satori"
from . import a
這句話表示導入test_import這個下面的a.py,但是直接像import sys那樣import a不行嗎?答案是不行的,至於為什么我們后面說。我們在__init__.py
中導入了sys模塊、a模塊,定義了name屬性,那么就等於將sys、a、name加入到了test_import這個包的local空間里面去了。因為我們說過,對於Python中的包,那么其等價於里面的__init__
文件,這個文件有什么,那么這個包就有什么。既然我們在__init__.py
中導入了sys、a模塊,定義了name,那么這個文件的屬性字典里面、或者也可以說local空間里面就有了"sys": sys, "a": a, "name": "satori"
這三個entry,而我們又說了__init__.py
里面有什么,那么通過包名就能夠調用什么。所以:
import test_import
print(test_import.a)
print(test_import.a.a)
print(test_import.sys)
print(test_import.name)
"""
>>>
<module 'test_import.a' from 'D:\\satori\\test_import\\a.py'>
123
<module 'sys' (built-in)>
satori
"""
# 首先在a里面有一個print(">>>")
# 而我們說初次導入一個模塊,就相當於把這個模塊里面的內容拿過來執行一遍;初次導入一個包則是把這個包里面的__init__.py拿過來執行一遍
# 那么在__init__里面導入a的時候,就會打印這個print
# 另外此時如果我再單獨導入test_import里面的a模塊的話,會怎么樣呢?
# 下面這兩種導入方式后面會介紹
import test_import.a
from test_import import a
# 我們看到a里面的print沒有被打印,證明確實模塊、包不管以怎樣的方式被導入,只要被導入一次,那么對應的文件只會被加載一遍
# 第二次導入只是將符號加入到了當前的名字空間中
相對導入與絕對導入
我們剛才使用了一個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 test_import
"""
import a
ModuleNotFoundError: No module named 'a'
"""
我們發現報錯了,告訴我們沒有a這個模塊,可是我們明明在module包里面定義了呀。還記得之前說的導入一個模塊、導入一個包會做哪些事情嗎?導入一個模塊,會將該模塊里面"拿過來"執行一遍,導入包會將該包里面的__init__.py
文件"拿過來"執行一遍。注意:我們把"拿過來"三個字加上了引號。
我們在test_import同級目錄的py文件中導入了test_import,那么就相當於把里面的__init__拿過來執行一遍(當然只有第一次導入的時候才會這么做)
,然后它們具有單獨的空間,是被隔離的,調用需要使用符號test_import來調用。但是正如我們之前所說,是"拿過來"執行,所以這個__init__.py
里面的內容是"拿過來",在當前的py文件(在哪里導入的就是哪里)
中執行的。所以由於import a這行代碼表示絕對導入,就相當於在當前模塊里面導入,會從sys.path里面搜索,但是a是在test_import包里面,那么此時還能找到這個a嗎?顯然是不能的。那from . import a
為什么就好使呢?因為這種導入表示相對導入,就表示要在__init__.py
所在目錄里面找,那么不管在什么地方導入這個包,由於這個__init__.py
的位置是不變的,所以from . import a
這種相對導入的方式總能找到對應的a。至於標准庫、第三方模塊、第三方包,因為它是在sys.path里面的,在哪兒都能找得到,所以可以絕對導入,貌似也只能絕對導入。並且我們知道每一個模塊都有一個__file__
屬性(除了內嵌在解釋器里面的模塊)
,當然包也是。如果你在一個模塊里面print(__file__)
,那么不管你在哪里導入這個模塊,打印的永遠是這個模塊的路徑;包的話,則是指向內部的__init__.py
文件。
另外關於相對導入,一個很重要的一點,一旦一個模塊出現了相對導入,那么這個模塊就不能被執行了,它只可以被導入。
import sys
from . import a
name = "satori"
"""
from . import a
ImportError: attempted relative import with no known parent package
"""
此時如果我試圖執行__init__.py
,那么就會給我報出這個錯誤。另外即便導入一個內部具有"相對導入"的模塊,那么此模塊和導入的模塊也不能在同一個包內,我們要執行的當前模塊至少要在導入模塊的上一級,否則執行的當前模塊也會報出這種錯誤。為什么會有這種情況,很簡單。想想為什么會有相對導入,就是希望這些模塊在被其它地方導入的時候能夠准確記住要導入的包的位置。那么這些模塊肯定要在一個共同的包里面,然后我們在包外面使用。所以我們導入一個具有相對導入的模塊時候,那么我們當前模塊和要導入的模塊絕對不能在同一個包里面。
import的另一種方式
我們要導入test_import包里面的a模塊,除了可以import test_import(_init__.py里面導入了a)
,還可以通過import test_import.a
的方式,另外如果是這種導入方式,那么module里面可以沒有__init__.py
文件,因為我們導入test_import包的時候,是通過test_import來獲取a,所以必須要有__init__.py
文件、並且里面導入a。但是在導入test_import.a的時候,就是找test_import.a,所以此時是可以沒有__init__.py
文件的。
# test_import/__init__.py
__version__ = "1.0"
# test_import/a.py
name = "夏色祭"
print("xxx")
此時test_import包里面的__init__.py
只定義了一個變量,下面我們來通過test_import.a的形式導入。
import test_import.a
print(test_import.a.name)
"""
xxx
夏色祭
"""
# 當import test_import.a的時候,會執行里面的print
# 然后可以通過test_import.a獲取a.py里面的屬性,這很好理解
# 但是,沒錯,我要說但是了
print(test_import.__version__) # 1.0
驚了,我們在導入test_import.a的時候,也把test_import導入進來了,為了更直觀的看到現象,我們在__init__.py
里面打印一句話。
# test_import/__init__.py
__version__ = "1.0"
print("我是test_import下面的__init__")
import test_import.a
"""
xxx
"""
所以一個有趣的現象就產生了,我們是導入test_import.a,但是把test_import也導入進來了。而且通過打印的順序,我們看到是先導入了test_import,然后再導入test_import下面的a。如果我們在__init__.py
中也導入了a會怎么樣?
# test_import/__init__.py
print("我是test_import下面的__init__")
from . import a
# test_import/a.py
print("我是test_import下面的a")
import test_import.a
"""
我是test_import下面的__init__
我是test_import下面的a
"""
# 我們看到a.py里面的內容只被打印了一次,說明沒有進行二次加載
# 在__init__.py中將a導進來之后,就加入到sys.modules里面了
所以通過test_import.a
的方式來導入,即使沒有__init__.py
文件依舊是可以訪問的,因為這是我在import的時候指定的。我們可以看一下sys.modules
import test_import.a
import sys
print(sys.modules["test_import"])
print(sys.modules["test_import.a"])
"""
我是test_import下面的__init__
我是test_import下面的a
<module 'test_import' from 'D:\\satori\\test_import\\__init__.py'>
<module 'test_import.a' from 'D:\\satori\\test_import\\a.py'>
"""
不過問題來了,為什么在導入test_import.a的時候,會將test_import也導入了進來呢?並且還可以直接使用test_import,畢竟這不是我們期望的結果,因為導入test_import.a的話,那么我們只是想使用test_import.a,不打算使用test_import,那么Python為什么要這么做呢?
事實上,這對Python而言是必須的,根據我們對Python虛擬機的執行原理的了解,Python要想執行test_import.a,那么肯定要先從local空間找到test_import,然后才能找到a,如果不找到test_import的話,那么對a的查找也就無從談起。可問題是sys.modules里面是
test_import.a
啊,這是一個整體啊。事實上盡管是一個整體,但並不是說有一個模塊,這個模塊就叫做test_import.a。准確的說import test_import.a表示先導入test_import,然后再將test_import下面的a加入到test_import的屬性字典里面。我們說當module這個包里面沒有__init__.py
的時候,那個這個包是無法使用的,因為屬性字典里面沒有相關屬性,但是當我們import test_import.a的時候,Python會先導入test_import這個包,然后自動幫我們把a這個模塊加入到module這個包的屬性字典里面。但之所以會有"test_import.a"這個key,顯然也是為了解決重復導入的問題。
假設test_import這個包里面有a和b兩個py文件,那么我們執行import test_import.a
和import test_import.b
會進行什么樣的動作應該就了如指掌了吧。執行import test_import.a
,那么會先導入test_import,然后把a加到test_import的屬性字典里面,執行import test_import.b
,還是會先導入包test_import,但是包test_import在上一步已經被導入了,所以此時直接會從sys.modules里面獲取,然后再把b加入到test_import的屬性字典里面。所以如果__init__.py
里面有一個print的話,那么兩次導入顯然只會print一次,這種現象是由Python對包中的模塊的動態加載機制決定的。還是那句話,一個包你就看成是里面的__init__.py
文件即可,Python對於包和模塊的區分不是特別明顯。
# test_import目錄下有__init__.py文件
import test_import
print(test_import.__file__) # D:\satori\test_import\__init__.py
print(test_import) # <module 'test_import' from 'D:\\satori\\test_import\\__init__.py'>
# test_import目錄下沒有__init__.py文件
import test_import
print(test_import.__file__) # None
print(test_import) # <module 'test_import' (namespace)>
我們看到如果包里面有__init__.py
文件,那么這個包的__file__
屬性就是其內部的__init__.py
文件,打印這個包,顯示的也是其內部的__init__.py
模塊。如果沒有__init__.py
文件,那么這個包的__file__
就是一個None,打印這個包,顯示其是一個名字空間。另外,我們知道任何一個模塊(即使里面什么也不寫)
的屬性字典里面都是有__builtins__
屬性的,因為可以直接使用內置的對象、函數等等。而__init__.py
也是屬於一個模塊,所以它也有__builtins__
屬性的,由於一個包指向了內部的__init__.py
,所以這個包的屬性字典也是有__builtins__
屬性的。但如果這個包沒有__init__.py
文件,那么這個包是沒有__builtins__
屬性的。
# 沒有__init__.py文件
import test_import
print(test_import.__dict__.get("__builtins__")) # None
# 有__init__.py文件
import test_import
print(test_import.__dict__.get("__builtins__")["int"]) # <class 'int'>
路徑搜索樹
假設我有這樣的一個目錄結構:
那么Python會將這個結構進行分解,得到一個類似於樹狀的節點集合:
然后從左到右依次去sys.modules中查找每一個符號所對應的module對象是否已經被加載,如果一個包被加載了,比如說包test_import被加載了,那么在包test_import對應的PyModuleObject中會維護一個元信息__path__
,表示這個包的路徑。比如我搜索A.a,當加載進來A的時候,那么a只會在A.__path__
中進行,而不會在Python的所有搜索路徑中執行了。
import test_import
print(test_import.__path__) # ['D:\\satori\\test_import']
# 導入sys模塊
try:
import test_import.sys
except ImportError as e:
print(e) # No module named 'test_import.sys'
# 顯然這樣是錯的,因為導入test_import.sys,那么就將搜索范圍只限定在test_import的__path__下面了
from與import
在Python的import中,有一種精確控制所加載的對象的方法,通過from和import的結合,可以只將我們期望的module對象、甚至是module對象中的某個符號,動態地加載到內存中。這種機制使得Python虛擬機在當前名字空間中引入的符號可以盡可能地少,從而更好地避免名字空間遭到污染。
按照我們之前所說,導入test_import下面的a模塊,我們可以使用import test_import.a
的方式,但是此時a是在test_import的名字空間中,不是在我們當前模塊的名字空間中。也就是說我們希望能直接通過符號a來調用,而不是test_import.a,此時通過from ... import ...
聯手就能完美解決。
from test_import import a
import sys
print(sys.modules.get("test_import") is not None) # True
print(sys.modules.get("test_import.a") is not None) # True
print(sys.modules.get("a") is not None) # False
我們看到,確確實實將a這個符號加載到當前的名字空間里面了,但是在sys.modules里面卻沒有a。還是之前說的,a這個模塊是在test_import這個包里面的,你不可能不通過包就直接拿到包里面的模塊,因此在sys.modules里面的形式其實還是test_import.a這樣形式,只不過在當前模塊的名字空間中是a,a被映射到sys.modules["test_import.a"],另外我們看到除了test_import.a,test_import也導入進來了,這個原因我們之前也說過了,不再贅述。所以我們發現即便我們是from ... import ...
,還是會觸發整個包的導入。只不過我們導入誰(假設從a導入b)
,就把誰加入到了當前模塊的名字空間里面(但是在sys.modules里面是沒有b的,而是a.b)
,並映射到sys.modules["a.b"]。
所以我們見識到了,即便是我們通過from test_import import a,還是會導入test_import這個包的,只不過test_import這個包是在sys.modules里面,並沒有暴露到local空間中。
此外我們from test_import import a
,導入的這個a是一個模塊,但是模塊a里面還有一個變量a,我們不加from,只通過import的話,那么最深也只能import到一個模塊,不可能說直接import模塊里面的某個變量、方法什么的。但是from ... import ...
的話,卻是可以的,比如我們from test_import.a import a
,這句就表示我要導入test_import.a模塊里面變量a。
from test_import.a import a
import sys
modules = sys.modules
print("a" in modules) # False
print("test_import.a" in modules) # True
print("test_import" in modules) # True
我們導入的a是一個變量,並不是模塊,所以sys.modules里面不會出現test_import.a.a這樣的東西存在,但是這個a畢竟是從test_import.a里面導入的,所以test_import.a是會在sys.modules里面的,同理test_import.a表示從test_import的屬性字典里面找a,所以test_import也是會進入sys.modules里面的。
最后還可以使用from test_import.a import *,這樣的機制把一個模塊里面所有的內容全部導入進來,本質和導入一個變量是一致的。但是在Python中有一個特殊的機制,比如我們from test_import.a import *,如果a里面定義了__all__,那么只會導入__all__里面指定的屬性。
# test_import/a.py
a = 123
b = 456
c = 789
__all__ = ["a", "b"]
我們注意到在__all__
里面只指定了a和b,那么后續通過from test_import.a import *
的時候,只會導入a和b,而不會導入c。
from test_import.a import *
print("a" in locals() and "b" in locals()) # True
print("c" in locals()) # False
from test_import.a import c
print("c" in locals()) # True
我們注意到:通過from ... import *
導入的時候,是無法導入c的,因為c沒有在__all__
中。但是即便如此,我們也可以通過單獨導入,把c導入進來。只是不推薦這么做,像pycharm這種智能編輯器也會提示:'c' is not declared in __all__
。因為既然沒有在__all__
里面,就證明這個變量是不希望被導入的,但是一般導入了也沒關系。
符號重命名
我們導入的時候一般為了解決符號沖突,往往會起別名,或者說符號重命名。比如包a和包b下面都有一個模塊叫做m,如果是from a import m
和from b import m
的話,那么兩者就沖突了,后面的m會把上面的m覆蓋掉,不然Python怎么知道要找哪一個m。所以這個時候我們會起別名,比如from a import m as m1
、from b import m as m2
,但是from a import *
是不支持as的。所以直接Python都是將module對象內部所以符號都暴露給了local名字空間,而符號重命名則是Python可以通過as關鍵字控制包、模塊、變量暴露給local名字空間的方式。
import test_import.a as a
print(a) # <module 'test_import.a' from 'D:\\satori\\test_import\\a.py'>
import sys
print("test_import.a" in sys.modules) # True
print("test_import" in sys.modules) # True
print("test_import" in locals()) # False
到結論我相信就應該心里有數了,不管我們有沒有as,既然import test_import.a
,那么sys.modules里面就一定有test_import.a,和test_import。其實理論上有包test_import就夠了,但是我們說a是一個模塊,為了避免多次導入所以也要加到sys.modules里面,而且a又是test_import包里面,所以是test_import.a。而我們這里as a
,那么a這個符號就暴露在了當前模塊的local空間里面,而且這個a就跟之前的test_import.a一樣,指向了test_import包下面的a模塊,無非是名字不同罷了。當然這不是重點,我們之前通過import test_import.a
的時候,會自動把test_import也加入到當前模塊的local空間里面,也就是說通過import test_import.a
是可以直接使用test_import的,但是當我們加上了as之后,發現test_import包已經不能訪問了。盡管都在sys.modules里面,但是對於加了as來說,此時的test_import這個包已經不在local名字空間里面了。一個as關鍵字,導致了兩者的不同,這是什么原因呢?我們后面分解。
符號的銷毀與重載
為了使用一個模塊,無論是內置的還是自己寫的,都需要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 test_import.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["test_import.a"])) # 1576944838432
import test_import.a as 我不叫a了
print(id(我不叫a了)) # 1576944838432
我們看到在del之后,a這個符號確實從local空間消失了,或者說dir已經看不到了。但是后面我們發現,消失的僅僅是a這個符號,至於test_import.a指向的PyModuleObject依舊在sys.modules里面巋然不動。然而,盡管它還存在於Python系統中,但是我們的程序再也無法感知到,但它就在那里不離不棄。所以此時Python就成功地向我們隱藏了這一切,我們的程序認為:test_import.a已經不存在了。
不過為什么Python要采用這種看上去類似模塊池
的緩存機制呢?因為組成一個完整系統的多個py文件可能都要對某個module對象進行import動作。所以要是從sys.modules里面刪除了,那么就意味着需要重新從文件里面讀取,如果不刪除,那么只需要從sys.modules里面暴露給當前的local名字空間即可。所以import實際上並不等同我們所說的動態加載,它的真實含義是希望某個模塊被感知,也就是"將這個模塊以某個符號的形式引入到某個名字空間"。這些都是同一個模塊,如果import等同於動態加載,那么Python對同一個模塊執行多次動態加載,並且內存中保存一個模塊的多個鏡像,這顯然是非常愚蠢的。
所以Python引入了全局的module對象集合--sys.modules,這個集合作為模塊池
,保存了模塊的唯一值。當某個模塊通過import聲明希望感知到某個module對象時,Python將在這個池子里面查找,如果被導入的模塊已經存在於池子中,那么就引入一個符號到當前模塊的名字空間中,並將其關聯到導入的模塊,使得被導入的模塊可以透過這個符號被當前模塊感知到。而如果被導入的模塊不在池子里,Python這才執行動態加載的動作。
如果這樣的話,難道一個模塊在被加載之后,就不能改變了。假如在加載了模塊a的時候,如果我們修改了模塊a,難道Python程序只能先暫停再重啟嗎?顯然不是這樣的,python的動態特性不止於此,它提供了一種重新加載的機制,使用importlib模塊,通過importlib.reload(module),可以實現重新加載並且這個函數是有返回值的,返回加載之后的模塊。
>>> import sys
>>> sys.path.append(r"D:\satori")
>>>
>>> from test_import import a
>>> a.name # 不存在name屬性
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: module 'test_import.a' has no attribute 'name'
>>>
>>>
>>> import importlib
>>> a = importlib.reload(a) # 增加一個賦值語句 name = "夏色祭"
>>> a.name
'夏色祭'
>>>
>>> a = importlib.reload(a) # 將 name = "夏色祭" 語句刪除
>>> a.name
'夏色祭'
>>>
首先我們的a模塊里面啥也沒有,但是我們在a.py里面增加了name變量,然后重新加載模塊,所以a.name正確打印。然后我們在a.py再刪除name屬性,然后重新加載,但是我們看到name變量還在里面,還可以被調用。
那么根據這個現象我們是不是可以大膽猜測,python在reload一個模塊的時候,只是將模塊里面新的符號加載進來,而刪除的則不管了,那么這個猜測到底正不正確呢,別急我們下面就來揭曉,並通過源碼來剖析import的實現機制。
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;//表示默認絕對導入
//從PyTupleObject中解析出需要的信息
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 *,通常用來將元組中的PyUnicodeObject對象解析成char *,i則用來將元組中的PyLongObject解析成int,而O則代表解析的目標對象依然是一個Python中的合法對象,通常這表示 PyArg_ParseTupleAndKeywords 不進行任何的解析和轉換,因為在PyTupleObject對象中存放的肯定是一個python的合法對象。至於|
和:
,它們不是非格式字符,而是指示字符,|
指示其后所帶的格式字符是可選的。也就是說,如果args中只有一個對象,那么__import__
對 PyArg_ParseTupleAndKeywords 的調用也不會失敗。其中,args中的那個對象會按照s
的指示被解析為char *,而剩下的globals、locals、fromlist則會按照O
的指示被初始化為Py_None,level是0。而:
則指示"格式字符"到此結束了,其后所帶字符串用於在解析過程中出錯時,定位錯誤的位置所使用的。
在完成了對參數的拆包動作之后,然后進入了 PyImport_ImportModuleLevelObject ,這個我們在import_name中已經看到了,而且它也是先獲取__builtin__
里面的__import__
函數指針。
另外每一個包和模塊都有一個__name__
和__path__
屬性
import numpy as np
import numpy.core
import six
print(np.__name__, np.__path__) # numpy ['C:\\python38\\lib\\site-packages\\numpy']
print(np.core.__name__, np.core.__path__) # numpy.core ['C:\\python38\\lib\\site-packages\\numpy\\core']
print(six.__name__, six.__path__) # six []
__name__
就是模塊或者包名,如果包下面的包或者模塊,那么就是包名.包名
或者包名.模塊名
,至於__path__
則是包所在的路徑。但是這個和__file__
又是不一樣的,如果是__file__
則是指向內部的__init__.py
文件,沒有則為None。但是對於模塊來說,則沒有__path__
。
精力有限,具體的不再深入。我們下面從python的角度來理解一下吧
Python中的import操作
import 模塊
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名字空間中。
import 包
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
"""
這是我們一開始考察的例子,現在我們已經很清楚地了解了IMPORT_NAME
的行為,在IMPORT_NAME
指令的最后,python虛擬機會將PyModuleObject
對象壓入到運行時棧內,隨后會將(sys, PyModuleObject)
存放到當前的local名字空間中。
如果涉及的是對包的import動作,那么IMPORT_NAME的指令參數則是關於包的完整路徑信息,IMPORT_NAME指令的內部將解析這個路徑,並為sklearn、sklearn.linear_model,sklearn.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_model和ridge已經在對應的包的屬性字典里面的了,我們通過sklearn一級一級往下找是可以找到的,因此只需要把skearn返回即可,或者說返回sklearn.linear_model.ridge本身就是不合理的,因為這表示導入一個名字就叫做sklearn.linear_model.ridge的模塊或者包,但顯然不存在,即便我們創建了,但是由於python的語法解析規范依舊不會得到想要的結果。不然的話,假設import test_import.a
,那Python是導入名為test_import.a
的模塊或包呢?還是導入test_import下的a呢?
也正如我們之前分析的test_import.a
,我們import test_import.a
的時候,會把test_import加載進來,然后把a加到test_import的屬性字典里面,然后只需要把test_import返回即可,因為我們通過test_import是可以找到a,而且也不存在我們期望的test_import.a
,因為這個test_import.a
代表的含義是從test_import的屬性字典里面獲取a
,所以import test_import.a
是必須要返回test_import的,而且只返回了test_import。至於sys.modules(一個字典)
里面是存在字符串名為test_import.a
的key的,這是為了避免重復加載所采取的策略,但它依舊表示從test_import里面獲取a。
import pandas.core
print(pandas.DataFrame({"a": [1, 2, 3]}))
"""
a
0 1
1 2
2 3
"""
# 導入pandas.core會先執行pandas的__init__文件
# 所以通過pandas.DataFrame是可以調用的
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了,而是一個元組。此時Python是將ridge放到了當前模塊的local空間中,並且sklearn、sklearn.linear_model都被導入了,並且存在於sys.modules里面,但是sklearn卻並不在當前local空間中,盡管這個對象被創建了,但是它被Python隱藏了。IMPORT_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
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類似,sklearn
,sklearn.linear_model
,sklearn.linear_model.ridge
都在sys.modules里面,但是我們加上了as xxx,那么這個xxx就直接指向了sklearn
下面的linear_model
下面的ridge
,就不需要sklearn了。這個和上面的from & import類似,只有xxx暴露在了當前模塊的local空間里面,sklearn雖然在sys.modules里面,但是在當前模塊就無法訪問了。
from & import & as
from sklearn.linear_model import ridge as xxx
這個我想連字節碼都不需要貼了,和之前from & import一樣,只是最后暴露給當前模塊的local空間的ridge變成了我們自己指定的xxx。
與module對象有關的名字空間問題
同函數、類一樣,每個PyModuleObject也是有自己的名字空間的。一個模塊不能直接訪問另一個模塊的內容,盡管模塊內部的作用域比較復雜,比如:遵循LEGB規則,但是模塊與模塊之間的划分則是很明顯的。
# test1.py
name = "夏色祭"
def print_name():
return name
# test2.py
from test1 import name, print_name
name = "神樂mea"
print(print_name()) # 夏色祭
執行test2.py之后,發現打印的依舊是"夏色祭"。我們說Python是根據LEGB規則,而print_name里面沒有name,那么去外層找,test2.py里面的name是"神樂mea",但是找到的依舊是test1.py里面的"夏色祭"。為什么?
還是那句話,模塊與模塊之間的作用域划分的非常明顯,print_name是test1.py里面的函數,所以在返回name的時候,只會從test1.py中搜索,無論如何都是不會跳過test1.py、跑到test2.py里面的。
再來看個栗子:
# test1.py
name = "夏色祭"
nicknames = ["夏哥", "祭妹"]
# test2.py
import test1
test1.name = "❤夏色祭❤"
test1.nicknames = ["祭妹"]
from test1 import name, nicknames
print(name) # ❤夏色祭❤
print(nicknames) # ['祭妹']
很簡單,直接把test1里面的變量修改了。因為這種方式,相當於直接修改test1內部的屬性字典。
# test1.py
name = "夏色祭"
nicknames = ["夏哥", "祭妹"]
# test2.py
from test1 import name, nicknames
name = "神樂mea"
nicknames.remove("夏哥")
from test1 import name, nicknames
print(name) # 夏色祭
print(nicknames) # ["祭妹"]
如果是from test1 import name, nicknames
,那么相當於在當前的local空間中創建一個變量name和nicknames指向對應的對象。name = "神樂mea"相當於重新賦值了,而nicknames.remove則是在本地進行修改。
小結
總的來說,Python中module對象的導入還是很簡單的,所以我們也沒有涉及太多關於源碼的知識。