咱們平常會寫各類各樣的python腳本,在運行的時候只須要輸入python xxx.py
程序就執行了。那麼問題就來了,一個py文件是如何被python變成一系列的機器指令並執行的呢?python
python的執行原理能夠用兩個詞來囊括:虛擬機、字節碼數組
首先在python中有一個很是關鍵的東西,這個東西被稱爲解釋器(interpreter),當咱們在命令行中輸入python時,就是爲了激活這個解釋器。固然若是後面還跟上了py文件,那麼解釋器會馬上被激活,而後執行py文件裏面的代碼。然而在真正開始執行以前,解釋器實際上還要完成一個很是複雜的工做--編譯py文件閉包
沒錯,python雖然是解釋型語言,但也是有編譯的過程的。不管執行哪個py文件,首先都是對源代碼進行編譯,編譯成一組python的字節碼(byte code),而後將編譯的字節碼交給python的虛擬機(virtual machine),而後由虛擬機按照順序一條一條地執行字節碼,從而完成對python執行動做。關於虛擬機和解釋器的區別,我的以爲在python中能夠認爲解釋器 = 編譯器 + 虛擬機。不要誤會這裏的編譯器,這只是編譯成python中的字節碼,可不是C語言中直接編譯成機器碼的解釋器。app
那麼這個python的編譯器和虛擬機藏身於什麼地方呢?咱們打開python的安裝目錄,會看到一個python.exe,點擊的時候確實能啓動一個終端,可是這個文件大小還不到100K,不可能容納一個解釋器加一個虛擬機。可是事實上你會發現,下面還有一個python37.dll,沒錯,編譯器、虛擬機都藏身於python37.dll當中。ide
咱們來看一個簡單的例子,來看看一個py文件被編譯以後應該產生一些什麼結果函數
class A: pass def foo(): pass a = A() foo()
首先咱們知道,python執行這個文件首先要進行的動做就是編譯,編譯的結果是字節碼。然而除了字節碼以外,還應該包含一些其餘的信息,這些結果也python運行的時候所必須的。lua
在編譯過程當中,像常量值、字符串這些源代碼當中的靜態信息都會被python編譯器收集起來,這些靜態信息都會體如今編譯以後的結果裏面。在python運行期間,這些源文件提供的靜態信息都會被存儲在一個運行時的對象當中,當python運行結束時,這個運行時對象中所包含的信息還會被存儲在一種文件中。這個對象和文件就是咱們接下來要探討的重點:PyCodeObject對象和pyc文件spa
咱們知道編譯的結果是一個pyc文件,可是裏面的內容是PyCodeObject對象,對於python編譯器來講,PyCodeObject對象纔是其真正的編譯結果,而pyc文件是這個對象在硬盤上表現形式。所以它們其實是python對源代碼編譯以後的兩種不一樣的存在形式。命令行
在程序運行期間,編譯結果存在於內存的PyCodeObject對象當中,而python結束運行以後,編譯結果又被保存到了pyc文件當中。當下一次運行的時候,python會根據pyc文件中記錄的編譯結果直接創建內存中的PyCodeObject對象,而不須要再度從新編譯了。debug
關於python是如何編譯py文件的,這個咱們不作介紹,由於這還涉及分詞、創建語法樹等等,咱們的重點是編譯以後的結果。要完全理解python虛擬機的運行時行爲,就必須完全理解PyCodeObject對象。
/* Bytecode object */ typedef struct { PyObject_HEAD int co_argcount; /* #arguments, except *args */ int co_kwonlyargcount; /* #keyword only arguments */ int co_nlocals; /* #local variables */ int co_stacksize; /* #entries needed for evaluation stack */ int co_flags; /* CO_..., see below */ int co_firstlineno; /* first source line number */ PyObject *co_code; /* instruction opcodes */ PyObject *co_consts; /* list (constants used) */ PyObject *co_names; /* list of strings (names used) */ PyObject *co_varnames; /* tuple of strings (local variable names) */ PyObject *co_freevars; /* tuple of strings (free variable names) */ PyObject *co_cellvars; /* tuple of strings (cell variable names) */ /* The rest aren't used in either hash or comparisons, except for co_name, used in both. This is done to preserve the name and line number for tracebacks and debuggers; otherwise, constant de-duplication would collapse identical functions/lambdas defined on different lines. */ Py_ssize_t *co_cell2arg; /* Maps cell vars which are arguments. */ PyObject *co_filename; /* unicode (where it was loaded from) */ PyObject *co_name; /* unicode (name, for reference) */ PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) See Objects/lnotab_notes.txt for details. */ void *co_zombieframe; /* for optimization only (see frameobject.c) */ PyObject *co_weakreflist; /* to support weakrefs to code objects */ /* Scratch space for extra data relating to the code object. Type is a void* to keep the format private in codeobject.c to force people to go through the proper APIs. */ void *co_extra; } PyCodeObject;
這裏面的每個域表明什麼,能夠暫時無需理會,咱們後面會一一介紹,可是裏面有一個co_code能夠提早劇透一下,這個域存放的是編譯所生成的字節碼指令序列。
python編譯器在對python源代碼進行編譯的時候,對於代碼中的每個block,都會建立一個PyCodeObject與之對應,但如何肯定多少代碼纔算是一個block呢?事實上,python有一個簡單而清晰的規則:當進入一個新的名字空間,或者說做用域時,咱們就算是進入了一個新的block了。
回顧以前建立的py文件,在編譯完以後會有三個PyCodeObject對象,一個是對應整個py文件的,一個是對應class A的,一個是對應def foo的。
在這裏,咱們開始說起python中一個相當重要的概念--命名空間(name space)。命名空間是符號的上下文環境,符號的含義取決於命名空間。更具體的說,一個變量名對應的變量值什麼,在python中是不肯定的,須要命名空間來決定。
對於某個符號,好比說a,在某個命名空間中,它多是一個PyLongObject對象;而在另外一個命名空間中,它多是一個PyListObject對象。可是在一個命名空間中,一個符號只能有一種含義。並且命名空間能夠一層套一層的造成一條命名空間鏈
,python虛擬機在執行的時候,會有很大一部分時間消耗在從命名空間鏈
中肯定一個符號所對應的對象是什麼。這也側面說明了,爲何python在建立變量的時候不須要指定類型、以及python爲何比較慢。
若是如今命名空間還不是很瞭解,沒關係,隨着剖析的深刻,你必定會對命名空間和python在命名空間鏈上的行爲有愈來愈深入的理解。總之如今須要記住的是:一個code block對應一個命名空間、同時也對應一個PyCodeObject對象。在python中,類、函數、module都對應着一個獨自的命名空間,所以都會有一個PyCodeObject與之對應。
每個PyCodeObject對象中都包含了每個code block中全部代碼通過編譯後獲得的byte code序列。前面咱們說到,python會將字節碼序列和PyCodeObject對象一塊兒存儲在pyc文件中。但不幸的是,事實並不老是這樣。有時,當咱們運行一個簡單的程序時並無產生pyc文件,所以咱們猜想:有些python程序只是臨時完成一些瑣碎的工做,這樣的程序僅僅只會運行一次,而後就不會再使用了,所以也就沒有保存至pyc文件的必要。
若是咱們在代碼中加上了一個import abc這樣語句,再執行你就會發現python爲其生成了pyc文件,這就說明import會觸發pyc的生成。實際上,在運行過程當中,若是碰到import abc這樣的語句,那麼python會在設定好的path中尋找abc.pyc或者abc.dll文件,若是沒有這些文件,而是隻發現了abc.py,那麼python會先將abc.py編譯成PyCodeObject,而後建立pyc文件,並將PyCodeObject寫到pyc文件裏面去。接下來,再對abc.pyc進行import動做,對,並非編譯成PyCodeObject對象以後直接使用,而是先寫到pyc裏面去,而後將pyc文件的PyCodeObject對象從新在內存中複製出來。
關於python的import機制,咱們後面章節會剖析,這裏只是用來完成pyc文件的觸發。固然獲得pyc文件有不少方法,好比使用py_compile模塊。
a.py
class A: a = 1
b.py
import a
執行b.py的時候,會發現建立了a.pyc。另外關於pyc文件的建立位置,會在當前文件的同級目錄下的__pycache__
目錄中建立,名字就叫作,py文件名.cpython-版本號.pyc
咱們使用notepad++打開,以二進制的方式查看一下,發現了一堆看上去毫無心義的數字。
所以python如何解釋這些字節流就相當重要了,這也是咱們關心的pyc文件的格式。
要了解pyc文件的格式,就必須清楚PyCodeObject對象中的每個域都表明什麼含義,這一點是不管如何也繞不過去的。
回顧一下,PyCodeObject結構體的定義:
PyObject_HEAD:真的是一切皆對象,字節碼也是一個對象,那麼天然要求PyObject這些頭部信息
co_argcount:位置參數個數
import inspect frame = None def foo(a, b, c, d): global frame frame = inspect.currentframe() foo(1, 2, 3, 4) # frame是這個函數的棧幀,這個棧幀先不用管 # 總之咱們再frame.f_code就能夠拿到字節碼,這個字節碼就是底層的PyCodeObject對象 # 咱們再調用一個co_argcount就能夠拿到位置參數的個數 print(frame.f_code.co_argcount) # 4
co_kwonlyargcount:只能用關鍵字參數的個數
def foo(a, b, *, c, d): global frame frame = inspect.currentframe() foo(1, 2, c=3, d=4) print(frame.f_code.co_argcount) # 2 print(frame.f_code.co_kwonlyargcount) # 2 # 咱們注意到c和d只能使用關鍵字參數傳遞,因此是2個 # 若是定義的時候不加*,即便你在在調用的時候都是使用關鍵字參數傳遞,也沒有用 # 都是屬於位置參數。 # co_kwonlyargcount是隻能用關鍵字參數傳遞的個數
co_nlocals:代碼塊中局部變量的個數,也包括參數
import inspect frame = None def foo(a, b, *, c): name = "xxx" age = 16 gender = "f" c = 33 global frame frame = inspect.currentframe() foo(1, 2, c=3) print(frame.f_code.co_nlocals) # 6 """ 參數有三個,加上局部變量3個,一共6個 下面的c=33中的c和參數的c總體是1個變量 """
co_stacksize:執行該段代碼塊須要的棧空間
import inspect frame = None def foo1(a, b): global frame frame = inspect.currentframe() foo1(1, 222) print(frame.f_code.co_stacksize) # 2
co_flags:用於mask,沒什麼用,這個能夠不用管
co_firstlineno:代碼塊在對應文件的起始行
若是foo被函數調用呢?
每一個函數都有本身獨自的命名空間,以及PyCodeObject對象,因此即使是經過bar調用的,co_firstlineno仍是自身的代碼塊的起始行
co_code:代碼塊編譯成字節碼的指令序列,以PyBytesObject的形式存在
import inspect frame = None def bar(): foo(1, 2) def foo(a, b): global frame frame = inspect.currentframe() bar() print(frame.f_code.co_code) # b't\x00\xa0\x01\xa1\x00a\x02d\x00S\x00'
co_consts:常量池,PyTupleObject對象,保存代碼塊中的全部常量。
import inspect frame = None def foo(a, b): name = "satori" global frame age = 16 frame = inspect.currentframe() foo(1, 2) print(frame.f_code.co_consts) # (None, 'satori', 16)
co_names:PyTupleObject對象,保存代碼塊中的全部符號
import inspect frame = None def foo(a, b): name = "satori" global frame age = 16 frame = inspect.currentframe() foo(1, 2) print(frame.f_code.co_names) # ('inspect', 'currentframe', 'frame')
co_varnames:代碼塊中出現的局部變量名
import inspect frame = None def foo(a, b): name = "satori" global frame age = 16 frame = inspect.currentframe() foo(1, 2) print(frame.f_code.co_varnames) # ('a', 'b', 'name', 'age')
co_freevars:python實現閉包所須要的東西。
import inspect frame = None def foo(a, b): name = "satori" global frame age = 16 frame = inspect.currentframe() foo(1, 2) print(frame.f_code.co_freevars) # ()
import inspect frame = None def foo(): name = "satori" age = 16 def inner(): global frame name age frame = inspect.currentframe() return inner foo()() print(frame.f_code.co_freevars) # ('age', 'name')
co_cellvars:內部嵌套函數所引用的外部函數的變量
import inspect frame = None def foo(): global frame name = "satori" age = 16 frame = inspect.currentframe() def inner(): name age return inner foo() # 注意到:這裏是foo(),不是foo()(),co_freevars須要的是內部函數的棧幀,因此咱們要調用兩次 # 但這裏co_cellvars須要的外部函數foo的棧幀,所以咱們只須要調用一次便可,由於global frame是在外部函數當中的 print(frame.f_code.co_cellvars) # ('age', 'name')
co_filename:代碼塊所在的文件名
import inspect frame = None def foo(): global frame name = "satori" age = 16 frame = inspect.currentframe() def inner(): name age return inner foo() print(frame.f_code.co_filename) # C:/Users/satori/Desktop/love_minami/a.py
co_name:代碼塊的名字,一般是函數名或者類名
import inspect frame = None def foo(): global frame name = "satori" age = 16 frame = inspect.currentframe() def inner(): name age return inner foo() print(frame.f_code.co_name) # foo
co_lnotab:字節碼指令與python源代碼的行號之間的對應關係,以PyByteObject的形式存在
import inspect frame = None def foo(): global frame name = "satori" age = 16 frame = inspect.currentframe() def inner(): name age return inner foo() print(frame.f_code.co_lnotab) # b'\x00\x03\x04\x01\x04\x01\x08\x02\x0e\x04'
事實上,python不會直接記錄這些信息,而是會記錄增量值。好比說:
目前PyCodeObject中的全部屬性咱們就介紹完了,事實上,還有那麼兩三個屬性咱們沒有介紹到,由於基本不用,而且經過frame.f_code去獲取也根本獲取不到。
事實上咱們已經介紹了一種方法去獲取相應的PyCodeObject對象,可是還有沒有其餘的方法呢?
__code__
def foo(): name = "satori" age = 16 code = foo.__code__ # 能夠看到,函數自己就提供了獲取PyCodeObject對象的接口,直接調用__code__便可 # 此時拿到的就是frame.f_code print(code.co_varnames) # ('name', 'age')
compile
在介紹compile以前,先介紹一下eval和exec。
eval:傳入一個字符串,而後把字符串裏面的內容拿出來。
a = 1 # 因此eval("a")就等價於a print(eval("a")) # 1 print(eval("1 + 1 + 1")) # 3 # 注意:eval是有返回值的,返回值就是字符串裏面內容。 # 或者說eval是能夠做爲右值的,好比a = eval("xxx") # 因此eval裏面毫不能夠出現諸如賦值之類的,好比 print(eval("a = 3")),那麼這個語句等價於print(a = 3),這樣顯然會出現語法錯誤的 # 所以eval裏面把字符串剝掉以後就是一個普通的值,不能夠出現諸如if、def等語句 try: eval("xxx") except NameError as e: print(e) # name 'xxx' is not defined
exec:傳入一個字符串,把字符串裏面的內容當成語句來執行,這個是沒有返回值,或者說返回值是None
exec("a = 1") # 等價於把a = 1這個字符串裏面的內容當成語句來執行 print(a) # 1 statement = """a = 123 if a == 123: print("a等於123") else: print("a不等於123") """ exec(statement) # a等於123 # 注意:'a等於123'並非exec返回的,而是把上面那坨字符串當成普通代碼執行的時候print出來的 # 這即是exec的做用。 # 那麼它和eval的區別就顯而易見的,eval是要求字符串裏面的內容可以當成一個值來打印,返回值就是裏面的值 # 而exec則是直接執行裏面的內容 # 舉個例子 print(eval("1 + 1")) # 2 print(exec("1 + 1")) # None exec("a = 1 + 1") print(a) # 2 try: eval("a = 1 + 1") except SyntaxError as e: print(e) # invalid syntax (<string>, line 1)`compile:至關於將二者組合起來`
compile則是拿到一個PyCodeObject對象
statement = "a, b = 1, 2" co = compile(statement, "hanser", "exec") print(co.co_name) # <module> print(co.co_filename) # hanser
前面咱們提到,python經過import module進行加載時,若是沒有找到相應的pyc或者dll文件,就會在py文件的基礎上自動建立pyc文件。因此想要了解pyc文件是怎麼建立的,只須要了解PyCodeObject是如何寫入的便可。關於寫入pyc文件,主要寫入三個內容:
magic number
這是python定義的一個整數值,不一樣版本的python會定義不一樣的magic number,這個值是爲了保證python可以加載正確的pyc。好比python3.7不會加載3.6版本的pyc,由於python在加載這個pyc文件的時候會首先檢測該pyc的magic number,如何和自身的magic number不一致,則拒絕加載。
pyc的建立時間
這個很好理解,由於編譯完以後要是把源代碼修改了怎麼辦呢?所以會判斷源代碼的最後修改時間和pyc文件的建立時間,若是pyc文件的建立時間比源代碼修改時間要早,說明在生成pyc以後,源代碼被修改了,那麼會從新編譯新的pyc,而反之則會直接加載pyc。
PyCodeObject對象
這個不用說了,確定是要存儲的。
文件對象:
//位置:Python/marshal.c //FILE是一個文件句柄,能夠把WFILE當作是FILE的包裝 typedef struct { FILE *fp; //文件句柄 //下面的字段在寫入信息的時候會看到 int error; int depth; PyObject *str; char *ptr; char *end; char *buf; _Py_hashtable_t *hashtable; int version; } WFILE;
寫入magic number和時間:
寫入magic number和時間都是調用了PyMarshal_WriteLongToFile
,咱們來看看長什麼樣子
void PyMarshal_WriteLongToFile(long x, FILE *fp, int version) { //聲明char型的數組,元素個數爲4個 char buf[4]; //聲明一個WFILE類型變量wf WFILE wf; //內存初始化 memset(&wf, 0, sizeof(wf)); //設置fp,文件句柄 wf.fp = fp; //將buf數組的指針賦值給wf.ptr和wf.buf wf.ptr = wf.buf = buf; //至關於buf的最後一個元素的指針 wf.end = wf.ptr + sizeof(buf); //寫錯誤 wf.error = WFERR_OK; //寫入版本信息 wf.version = version; //調用w_long將x也就是版本信息或者時間寫到wf裏面去 w_long(x, &wf); //刷到磁盤上 w_flush(&wf); } //因此咱們看到這一步只是初始化一個WFILE對象,真正寫入則是調用w_long static void w_long(long x, WFILE *p) { w_byte((char)( x & 0xff), p); w_byte((char)((x>> 8) & 0xff), p); w_byte((char)((x>>16) & 0xff), p); w_byte((char)((x>>24) & 0xff), p); } //w_long則是將要寫入的x一個字節一個字節寫到文件裏面去。
寫入PyCodeObject對象:
寫入PyCodeObject對象則是調用了PyMarshal_WriteObjectToFile
,咱們也來看看長什麼樣子
void PyMarshal_WriteObjectToFile(PyObject *x, FILE *fp, int version) { char buf[BUFSIZ]; WFILE wf; memset(&wf, 0, sizeof(wf)); wf.fp = fp; wf.ptr = wf.buf = buf; wf.end = wf.ptr + sizeof(buf); wf.error = WFERR_OK; wf.version = version; if (w_init_refs(&wf, version)) return; /* caller mush check PyErr_Occurred() */ w_object(x, &wf); w_clear_refs(&wf); w_flush(&wf); } //能夠看到,和PyMarshal_WriteLongToFile基本是相似的 //只不過PyMarshal_WriteLongToFile調用的是w_long,而PyMarshal_WriteObjectToFile調用的是w_object static void w_object(PyObject *v, WFILE *p) { char flag = '\0'; p->depth++; if (p->depth > MAX_MARSHAL_STACK_DEPTH) { p->error = WFERR_NESTEDTOODEEP; } else if (v == NULL) { w_byte(TYPE_NULL, p); } else if (v == Py_None) { w_byte(TYPE_NONE, p); } else if (v == PyExc_StopIteration) { w_byte(TYPE_STOPITER, p); } else if (v == Py_Ellipsis) { w_byte(TYPE_ELLIPSIS, p); } else if (v == Py_False) { w_byte(TYPE_FALSE, p); } else if (v == Py_True) { w_byte(TYPE_TRUE, p); } else if (!w_ref(v, &flag, p)) w_complex_object(v, flag, p); p->depth--; }
能夠看到本質上仍是調用了w_byte,可是在這裏面咱們並無看到諸如:list、tuple之類的數據的存儲過程,注意最後的w_complex_object,關鍵來了
//源代碼很長 static void w_complex_object(PyObject *v, char flag, WFILE *p) { Py_ssize_t i, n; if (PyLong_CheckExact(v)) { long x = PyLong_AsLong(v); if ((x == -1) && PyErr_Occurred()) { PyLongObject *ob = (PyLongObject *)v; PyErr_Clear(); w_PyLong(ob, flag, p); } else { #if SIZEOF_LONG > 4 long y = Py_ARITHMETIC_RIGHT_SHIFT(long, x, 31); if (y && y != -1) { /* Too large for TYPE_INT */ w_PyLong((PyLongObject*)v, flag, p); } else #endif { W_TYPE(TYPE_INT, p); w_long(x, p); } } } else if (PyFloat_CheckExact(v)) { if (p->version > 1) { unsigned char buf[8]; if (_PyFloat_Pack8(PyFloat_AsDouble(v), buf, 1) < 0) { p->error = WFERR_UNMARSHALLABLE; return; } W_TYPE(TYPE_BINARY_FLOAT, p); w_string((char*)buf, 8, p); } else { char *buf = PyOS_double_to_string(PyFloat_AS_DOUBLE(v), 'g', 17, 0, NULL); if (!buf) { p->error = WFERR_NOMEMORY; return; } n = strlen(buf); W_TYPE(TYPE_FLOAT, p); w_byte((int)n, p); w_string(buf, n, p); PyMem_Free(buf); } } else if (PyComplex_CheckExact(v)) { if (p->version > 1) { unsigned char buf[8]; if (_PyFloat_Pack8(PyComplex_RealAsDouble(v), buf, 1) < 0) { p->error = WFERR_UNMARSHALLABLE; return; } W_TYPE(TYPE_BINARY_COMPLEX, p); w_string((char*)buf, 8, p); if (_PyFloat_Pack8(PyComplex_ImagAsDouble(v), buf, 1) < 0) { p->error = WFERR_UNMARSHALLABLE; return; } w_string((char*)buf, 8, p); } else { char *buf; W_TYPE(TYPE_COMPLEX, p); buf = PyOS_double_to_string(PyComplex_RealAsDouble(v), 'g', 17, 0, NULL); if (!buf) { p->error = WFERR_NOMEMORY; return; } n = strlen(buf); w_byte((int)n, p); w_string(buf, n, p); PyMem_Free(buf); buf = PyOS_double_to_string(PyComplex_ImagAsDouble(v), 'g', 17, 0, NULL); if (!buf) { p->error = WFERR_NOMEMORY; return; } n = strlen(buf); w_byte((int)n, p); w_string(buf, n, p); PyMem_Free(buf); } } else if (PyBytes_CheckExact(v)) { W_TYPE(TYPE_STRING, p); w_pstring(PyBytes_AS_STRING(v), PyBytes_GET_SIZE(v), p); } else if (PyUnicode_CheckExact(v)) { if (p->version >= 4 && PyUnicode_IS_ASCII(v)) { int is_short = PyUnicode_GET_LENGTH(v) < 256; if (is_short) { if (PyUnicode_CHECK_INTERNED(v)) W_TYPE(TYPE_SHORT_ASCII_INTERNED, p); else W_TYPE(TYPE_SHORT_ASCII, p); w_short_pstring((char *) PyUnicode_1BYTE_DATA(v), PyUnicode_GET_LENGTH(v), p); } else { if (PyUnicode_CHECK_INTERNED(v)) W_TYPE(TYPE_ASCII_INTERNED, p); else W_TYPE(TYPE_ASCII, p); w_pstring((char *) PyUnicode_1BYTE_DATA(v), PyUnicode_GET_LENGTH(v), p); } } else { PyObject *utf8; utf8 = PyUnicode_AsEncodedString(v, "utf8", "surrogatepass"); if (utf8 == NULL) { p->depth--; p->error = WFERR_UNMARSHALLABLE; return; } if (p->version >= 3 && PyUnicode_CHECK_INTERNED(v)) W_TYPE(TYPE_INTERNED, p); else W_TYPE(TYPE_UNICODE, p); w_pstring(PyBytes_AS_STRING(utf8), PyBytes_GET_SIZE(utf8), p); Py_DECREF(utf8); } } else if (PyTuple_CheckExact(v)) { n = PyTuple_Size(v); if (p->version >= 4 && n < 256) { W_TYPE(TYPE_SMALL_TUPLE, p); w_byte((unsigned char)n, p); } else { W_TYPE(TYPE_TUPLE, p); W_SIZE(n, p); } for (i = 0; i < n; i++) { w_object(PyTuple_GET_ITEM(v, i), p); } } else if (PyList_CheckExact(v)) { W_TYPE(TYPE_LIST, p); n = PyList_GET_SIZE(v); W_SIZE(n, p); for (i = 0; i < n; i++) { w_object(PyList_GET_ITEM(v, i), p); } } else if (PyDict_CheckExact(v)) { Py_ssize_t pos; PyObject *key, *value; W_TYPE(TYPE_DICT, p); /* This one is NULL object terminated! */ pos = 0; while (PyDict_Next(v, &pos, &key, &value)) { w_object(key, p); w_object(value, p); } w_object((PyObject *)NULL, p); } else if (PyAnySet_CheckExact(v)) { PyObject *value, *it; if (PyObject_TypeCheck(v, &PySet_Type)) W_TYPE(TYPE_SET, p); else W_TYPE(TYPE_FROZENSET, p); n = PyObject_Size(v); if (n == -1) { p->depth--; p->error = WFERR_UNMARSHALLABLE; return; } W_SIZE(n, p); it = PyObject_GetIter(v); if (it == NULL) { p->depth--; p->error = WFERR_UNMARSHALLABLE; return; } while ((value = PyIter_Next(it)) != NULL) { w_object(value, p); Py_DECREF(value); } Py_DECREF(it); if (PyErr_Occurred()) { p->depth--; p->error = WFERR_UNMARSHALLABLE; return; } } else if (PyCode_Check(v)) { PyCodeObject *co = (PyCodeObject *)v; W_TYPE(TYPE_CODE, p); w_long(co->co_argcount, p); w_long(co->co_kwonlyargcount, p); w_long(co->co_nlocals, p); w_long(co->co_stacksize, p); w_long(co->co_flags, p); w_object(co->co_code, p); w_object(co->co_consts, p); w_object(co->co_names, p); w_object(co->co_varnames, p); w_object(co->co_freevars, p); w_object(co->co_cellvars, p); w_object(co->co_filename, p); w_object(co->co_name, p); w_long(co->co_firstlineno, p); w_object(co->co_lnotab, p); } else if (PyObject_CheckBuffer(v)) { /* Write unknown bytes-like objects as a bytes object */ Py_buffer view; if (PyObject_GetBuffer(v, &view, PyBUF_SIMPLE) != 0) { w_byte(TYPE_UNKNOWN, p); p->depth--; p->error = WFERR_UNMARSHALLABLE; return; } W_TYPE(TYPE_STRING, p); w_pstring(view.buf, view.len, p); PyBuffer_Release(&view); } else { W_TYPE(TYPE_UNKNOWN, p); p->error = WFERR_UNMARSHALLABLE; } }
源代碼很長,這裏就不一一分析了。雖然長,可是邏輯很簡單,就是對應不一樣的對象、執行不一樣的寫的動做。然而其最終目的都是經過w_byte寫到pyc文件中。換句話說,python在往pyc寫入list對象時,只是將list中包含的數值或者字符串等對象寫到了pyc文件中。同時這也意味着,python在加載pyc文件時,必須基於這些數值或字符串從新構造出list對象。
對於PyCodeObject對象,很顯然,w_object會遍歷PyCodeObject中的全部域,將這些域依次寫入
PyCodeObject *co = (PyCodeObject *)v; W_TYPE(TYPE_CODE, p); w_long(co->co_argcount, p); w_long(co->co_kwonlyargcount, p); w_long(co->co_nlocals, p); w_long(co->co_stacksize, p); w_long(co->co_flags, p); w_object(co->co_code, p); w_object(co->co_consts, p); w_object(co->co_names, p); w_object(co->co_varnames, p); w_object(co->co_freevars, p); w_object(co->co_cellvars, p); w_object(co->co_filename, p); w_object(co->co_name, p); w_long(co->co_firstlineno, p); w_object(co->co_lnotab, p);
可是當面對一個PyListObject對象時,會有什麼變化呢?沒錯,會和PyCodeObject同樣,w_object仍是會遍歷,而後將PyListObject對象中的每個元素依次寫入到pyc文件中。
//能夠看到PyTupleObject、PyListObject、PyDictObject都是採用了相同的姿式 //注意裏面的W_TYPE else if (PyTuple_CheckExact(v)) { n = PyTuple_Size(v); if (p->version >= 4 && n < 256) { W_TYPE(TYPE_SMALL_TUPLE, p); w_byte((unsigned char)n, p); } else { W_TYPE(TYPE_TUPLE, p); W_SIZE(n, p); } for (i = 0; i < n; i++) { w_object(PyTuple_GET_ITEM(v, i), p); } } else if (PyList_CheckExact(v)) { W_TYPE(TYPE_LIST, p); n = PyList_GET_SIZE(v); W_SIZE(n, p); for (i = 0; i < n; i++) { w_object(PyList_GET_ITEM(v, i), p); } } else if (PyDict_CheckExact(v)) { Py_ssize_t pos; PyObject *key, *value; W_TYPE(TYPE_DICT, p); /* This one is NULL object terminated! */ pos = 0; while (PyDict_Next(v, &pos, &key, &value)) { w_object(key, p); w_object(value, p); } w_object((PyObject *)NULL, p); }
咱們看到不管對於哪個對象,在寫入以前,都會先調用W_TYPE寫一個相似於類型的東西,是的,諸如TYPE_LIST、TYPE_TUPLE、TYPE_DICT這樣的標識,對於pyc文件的加載起着相當重要的做用。
以前說過,python僅僅將數值和字符串寫入到pyc文件。當PyCodeObject寫入到pyc以後,全部的數據就變成了了字節流,類型信息就丟失了。然鵝若是沒有類型信息,那麼當python再次加載pyc文件的時候,就沒辦法知道字節流中隱藏的結構和蘊含的信息,因此python必須往pyc文件寫入一個標識,這些標識正是python定義的類型信息,若是python在pyc中發現了這樣的標識,則預示着上一個對象結束,新的對象開始,而且也知道新對象是什麼樣的對象,從而也知道該執行什麼樣的加載動做。這些標識也是能夠看到的
//marshal.c #define TYPE_NULL '0' #define TYPE_NONE 'N' #define TYPE_FALSE 'F' #define TYPE_TRUE 'T' #define TYPE_STOPITER 'S' #define TYPE_ELLIPSIS '.' #define TYPE_INT 'i' /* TYPE_INT64 is not generated anymore. Supported for backward compatibility only. */ #define TYPE_INT64 'I' #define TYPE_FLOAT 'f' #define TYPE_BINARY_FLOAT 'g' #define TYPE_COMPLEX 'x' #define TYPE_BINARY_COMPLEX 'y' #define TYPE_LONG 'l' #define TYPE_STRING 's' #define TYPE_INTERNED 't' #define TYPE_REF 'r' #define TYPE_TUPLE '(' #define TYPE_LIST '[' #define TYPE_DICT '{' #define TYPE_CODE 'c' #define TYPE_UNICODE 'u' #define TYPE_UNKNOWN '?' #define TYPE_SET '<' #define TYPE_FROZENSET '>'
到了這裏能夠看到,其實python對於PyCodeObject對象的導出其實是不復雜的,實際上無論什麼對象,最後都爲歸結爲兩種簡單的形式,一種是數值寫入,一種是字符串寫入。上面都是對數值的寫入,比較簡單,僅僅須要按照字節一次寫入pyc便可。然而在寫入字符串的時候,python設計了一種比較複雜的機制,有興趣能夠本身閱讀源碼,這裏再也不介紹。
# a.py class A: pass def foo(): pass a = A() foo()
咱們以前說對於這樣的一個py文件,會建立三個PyCodeObject對象,可是寫到pyc文件裏面的只有一個PyCodeObject,這難道不就意味着有兩個PyCodeObject丟失了嗎?其實很明顯,有兩個PyCodeObject對象是位於另外一個PyCodeObject對象當中的。所以其實foo和A對應的PyCodeObject對象是位於a.py這個PyCodeObject對象當中的,準確的說是位於co_consts當中
在將一個PyCodeObject對象寫入到pyc文件當中時,若是碰到了包含的另外一個PyCodeObject對象,那麼就會遞歸地執行寫入PyCodeObject對象的操做。如此下去,最終全部的PyCodeObject對象都會寫入到pyc文件當中,所以pyc文件當中的PyCodeObject對象也是以一種嵌套的關係聯繫在一塊兒的。
關於python的字節碼,是後面章節剖析虛擬機的重點,如今先來看一下。咱們知道python執行源代碼以前會對其進行編譯獲得字節碼序列,python虛擬機會根據這些字節碼序列來進行一系列的操做,從而完成對程序的執行。
在python中一共定義了130條指令
#define POP_TOP 1 #define ROT_TWO 2 #define ROT_THREE 3 #define DUP_TOP 4 #define DUP_TOP_TWO 5 #define NOP 9 #define UNARY_POSITIVE 10 #define UNARY_NEGATIVE 11 #define UNARY_NOT 12 #define UNARY_INVERT 15 #define BINARY_MATRIX_MULTIPLY 16 #define INPLACE_MATRIX_MULTIPLY 17 #define BINARY_POWER 19 #define BINARY_MULTIPLY 20 #define BINARY_MODULO 22 #define BINARY_ADD 23 #define BINARY_SUBTRACT 24 #define BINARY_SUBSCR 25 #define BINARY_FLOOR_DIVIDE 26 #define BINARY_TRUE_DIVIDE 27 #define INPLACE_FLOOR_DIVIDE 28 ... ...
若是使用過dis模塊的小夥伴確定很熟悉,固然這裏咱們只是先看一下, 後面章節會介紹。
結尾的圖片裏面的妹子叫hanser,我的很是喜歡的一個up主(開嬰兒車的老司機),唱歌超好聽的,能夠去B站關注一下她,能給你帶來不少歡樂哦。
連接(hanser嗶哩嗶哩我的主頁):https://space.bilibili.com/11073?from=search&seid=542967129544922757