《python解釋器源碼剖析》第8章--python的字節碼與pyc文件

8.0 序

咱們平常會寫各類各樣的python腳本,在運行的時候只須要輸入python xxx.py程序就執行了。那麼問題就來了,一個py文件是如何被python變成一系列的機器指令並執行的呢?python

8.1 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

8.2 python編譯器的編譯結果--PyCodeObject對象

8.2.1 PyCodeObject對象和pyc文件

咱們來看一個簡單的例子,來看看一個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

8.2.2 python源碼中的PyCodeObject對象

關於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與之對應。

8.2.3 pyc文件

每個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去獲取也根本獲取不到。

8.2.4 在python中訪問PyCodeObject對象

事實上咱們已經介紹了一種方法去獲取相應的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

8.3 pyc文件的生成

8.3.1 建立pyc文件的具體過程

前面咱們提到,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設計了一種比較複雜的機制,有興趣能夠本身閱讀源碼,這裏再也不介紹。

8.3.2 PyCodeObject的寫入

# 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對象也是以一種嵌套的關係聯繫在一塊兒的。

8.4 python的字節碼

關於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

相關文章
相關標籤/搜索