Python 編譯:code對象 與 pyc文件

運行程序

當在shell中敲入python xx.py運行 Python 程序時悲雳,就是激活了 Python 解釋器置侍。

Python 解釋器并不會立即運行程序映之,而是會對 Python 程序的源代碼進行編譯拦焚,產(chǎn)生字節(jié)碼,然后將字節(jié)碼交給虛擬機一條條順序執(zhí)行杠输。

源文件中的內(nèi)容可以分為:字符串赎败、常量操作蠢甲。

操作會被編譯為字節(jié)碼指令序列僵刮,字符串常量在編譯的過程中會被收集起來。這些編譯后的信息在程序運行時鹦牛,會作為 運行時對象 PyCodeObject 存儲于內(nèi)存中搞糕。運行結(jié)束后,PyCodeObject 被放入xx.pyc文件曼追,保存在硬盤中窍仰。這樣,在下次運行時礼殊,可以直接根據(jù).pyc文件的內(nèi)容驹吮,在內(nèi)存中建立 PyCodeObject ,不需要再進行編譯晶伦。

PyCodeObject

在編譯器對源碼進行編譯時碟狞,會為每一個 Code Block 創(chuàng)建一個對應(yīng)的 PyCodeObject。那么婚陪,什么是 Code Block 呢族沃?規(guī)則是:當進入一個新的名字空間,或者新的作用域泌参,就是進入了一個新 Code Block脆淹。名字空間是符號的上下文環(huán)境,決定了符號的含義沽一。也就是說未辆,決定了變量名對應(yīng)的變量值是什么。

名字空間是可以嵌套的锯玛,能夠形成一個名字空間鏈咐柜,虛擬機在執(zhí)行字節(jié)碼時,一個重要的任務(wù)就是從鏈中確定一個符號的對象是什么攘残。

在 Python 中拙友,類、函數(shù)歼郭、modules 對應(yīng)獨立的名字空間遗契,所以都有對應(yīng)的 PyCodeObject。

PyCodeObject 中co_code域保存的就是對操作編譯生成的字節(jié)碼指令序列病曾。

產(chǎn)生pyc文件的方法

上面提到牍蜂,Python 程序運行結(jié)束后漾根,會在硬盤中以.pyc文件的形式存儲 PyCodeObject,但直接運行 Python 程序并不會產(chǎn)生.pyc文件鲫竞。

這可能是因為直接運行的 Python 程序辐怕,有些只是臨時使用一次,所以沒有通過.pyc保存編譯結(jié)果的必要从绘。

一種常見的寄疏,產(chǎn)生pyc文件的方法是import機制。當Python 程序運行時僵井,如果遇到 import abc陕截,會到設(shè)定好的path中尋找 abc.pyc 文件,如果沒有批什,只找到abc.py农曲,會先將 abc.py 編譯成 CodeObject,然后創(chuàng)建 pyc 文件驻债,將 CodeObject寫入朋蔫,最后才會對 pyc 進行import操作,將 pyc 中的 CodeObject重新復(fù)制到內(nèi)存却汉,并運行。

另外荷并,Python 標準庫中的py_compilecompile可以幫助手動產(chǎn)生 pyc 文件合砂。

pyc 文件內(nèi)容是二進制的,想要了解 pyc 文件的格式源织,就要了解 PyCodeObject 中各個域的作用翩伪。

PyCodeObject域

在 Python 中訪問 PyCodeObject

C語言形式的 PyCodeObject 對應(yīng) Python 中的 Code對象,Code對象 是對 PyCodeObject 的簡單包裝谈息。

因此缘屹,可以通過 Code對象 訪問 PyCodeObject 的各個域。這就需要使用 內(nèi)建函數(shù) compile侠仇。

test.py

import sys

a = 1

def b():
    print a
    a = 2
    print a
>>> source = open('/Users/chao/Desktop/test.py').read()
>>> co = compile(source, 'test.py', 'exec')
>>> type(co)
<type 'code'>
>>> dir(co)
['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
>>> print co.co_names
()
>>> print co.co_name
<module>
>>> print co.co_filename
test.py

創(chuàng)建 pyc 文件

一個 pyc 文件包含三部分獨立的信息:

  • magic number
  • pyc 文件的創(chuàng)建時間信息
  • PyCodeObject

import.c

static void
write_compiled_module(PyCodeObject *co, char *cpathname, struct stat *srcstat)
{
    FILE *fp;
    time_t mtime = srcstat->st_mtime;
#ifdef MS_WINDOWS   /* since Windows uses different permissions  */
    mode_t mode = srcstat->st_mode & ~S_IEXEC;
#else
    mode_t mode = srcstat->st_mode & ~S_IXUSR & ~S_IXGRP & ~S_IXOTH;
#endif

    fp = open_exclusive(cpathname, mode);
    if (fp == NULL) {
        if (Py_VerboseFlag)
            PySys_WriteStderr(
                "# can't create %s\n", cpathname);
        return;
    }
    PyMarshal_WriteLongToFile(pyc_magic, fp, Py_MARSHAL_VERSION);     # 寫入`magic number`
    /* First write a 0 for mtime */
    PyMarshal_WriteLongToFile(0L, fp, Py_MARSHAL_VERSION);
    PyMarshal_WriteObjectToFile((PyObject *)co, fp, Py_MARSHAL_VERSION);  # 寫入`PyCodeObject`
    if (fflush(fp) != 0 || ferror(fp)) {
        if (Py_VerboseFlag)
            PySys_WriteStderr("# can't write %s\n", cpathname);
        /* Don't keep partial file */
        fclose(fp);
        (void) unlink(cpathname);
        return;
    }
    /* Now write the true mtime */
    fseek(fp, 4L, 0);
    assert(mtime < LONG_MAX);
    PyMarshal_WriteLongToFile((long)mtime, fp, Py_MARSHAL_VERSION);   # 寫入 pyc 創(chuàng)建時間
    fflush(fp);
    fclose(fp);
    if (Py_VerboseFlag)
        PySys_WriteStderr("# wrote %s\n", cpathname);
}

下面一一進行說明

1轻姿,magic number
是 Python 定義的一個整數(shù)值,不同版本定義不同逻炊,用來確保 Python 的兼容性互亮。Python 在加載 pyc 時首先檢查 magic number ,如果與 Python 自身的 magic number 不同余素,說明創(chuàng)建 pyc 的 Python 版本 與 當前版本不兼容豹休,會拒絕加載。

為什么會不兼容呢桨吊?因為字節(jié)碼指令發(fā)生了變化威根,有刪除或增加凤巨。

/* Magic word to reject .pyc files generated by other Python versions.
   It should change for each incompatible change to the bytecode.

   The value of CR and LF is incorporated so if you ever read or write
   a .pyc file in text mode the magic number will be wrong; also, the
   Apple MPW compiler swaps their values, botching string constants.

   The magic numbers must be spaced apart atleast 2 values, as the
   -U interpeter flag will cause MAGIC+1 being used. They have been
   odd numbers for some time now.

   There were a variety of old schemes for setting the magic number.
   The current working scheme is to increment the previous value by
   10.

   Known values:
       Python 1.5:   20121
       Python 1.5.1: 20121
       Python 1.5.2: 20121
       Python 1.6:   50428
       Python 2.0:   50823
       Python 2.0.1: 50823
       Python 2.1:   60202
       Python 2.1.1: 60202
       Python 2.1.2: 60202
       Python 2.2:   60717
       Python 2.3a0: 62011
       Python 2.3a0: 62021
       Python 2.3a0: 62011 (!)
       Python 2.4a0: 62041
       Python 2.4a3: 62051
       Python 2.4b1: 62061
       Python 2.5a0: 62071
       Python 2.5a0: 62081 (ast-branch)
       Python 2.5a0: 62091 (with)
       Python 2.5a0: 62092 (changed WITH_CLEANUP opcode)
       Python 2.5b3: 62101 (fix wrong code: for x, in ...)
       Python 2.5b3: 62111 (fix wrong code: x += yield)
       Python 2.5c1: 62121 (fix wrong lnotab with for loops and
                            storing constants that should have been removed)
       Python 2.5c2: 62131 (fix wrong code: for x, in ... in listcomp/genexp)
       Python 2.6a0: 62151 (peephole optimizations and STORE_MAP opcode)
       Python 2.6a1: 62161 (WITH_CLEANUP optimization)
.
*/
#define MAGIC (62161 | ((long)'\r'<<16) | ((long)'\n'<<24))

/* Magic word as global; note that _PyImport_Init() can change the
   value of this global to accommodate for alterations of how the
   compiler works which are enabled by command line switches. */
static long pyc_magic = MAGIC;

2,pyc 創(chuàng)建時間
使得 Python 自動將 pyc 文件與最新的 Python 文件同步洛搀。當對 Python 程序進行編譯產(chǎn)生 pyc 后敢茁,如果后來進行了修改,此時 Python 在嘗試加載 pyc 時姥卢,會發(fā)現(xiàn) pyc 創(chuàng)建時間早于 Python 程序卷要,于是將重新編譯,生成新的 pyc 文件独榴。

3僧叉,PyCodeObject
編譯器會遍歷 PyCodeObject 中的所有域,并依次寫入 pyc棺榔。對于 PyCodeObject 中的每一個對象瓶堕,同樣會進行遍歷,并寫入類型標志數(shù)據(jù)(數(shù)值/字符串)

類型標志的三個作用:表明上一個對象的結(jié)束症歇、新對象的開始郎笆、確定新對象的類型

marshal.h,類型標志

#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'
#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_STRINGREF          '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          '>'

向 pyc 寫入字符串

部分略

對于嵌套的名字空間忘晤,產(chǎn)生的 PyCodeObject 也是遞歸嵌套的宛蚓,嵌套的 PyCodeObject 在上層 PyCodeObject 的co_consts中。

字節(jié)碼

源代碼編譯為 字節(jié)碼指令 序列设塔,虛擬機根據(jù)字節(jié)碼進行操作凄吏,完成程序的執(zhí)行,opcode.h中定義了當前版本 Python 支持的字節(jié)碼指令闰蛔。

字節(jié)碼指令 的編碼并不是按順序增長的痕钢,中間有跳躍。

Include目錄下的opcode.h定義了字節(jié)碼指令

#define STOP_CODE   0
#define POP_TOP     1
#define ROT_TWO     2
#define ROT_THREE   3
#define DUP_TOP     4
#define ROT_FOUR    5
#define NOP     9

#define UNARY_POSITIVE  10
#define UNARY_NEGATIVE  11
#define UNARY_NOT   12
#define UNARY_CONVERT   13

#define UNARY_INVERT    15

#define LIST_APPEND 18
#define BINARY_POWER    19

#define BINARY_MULTIPLY 20
#define BINARY_DIVIDE   21
#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
#define INPLACE_TRUE_DIVIDE 29

#define SLICE       30
/* Also uses 31-33 */

#define STORE_SLICE 40
/* Also uses 41-43 */

#define DELETE_SLICE    50
/* Also uses 51-53 */

#define STORE_MAP   54
#define INPLACE_ADD 55
#define INPLACE_SUBTRACT    56
#define INPLACE_MULTIPLY    57
#define INPLACE_DIVIDE  58
#define INPLACE_MODULO  59
#define STORE_SUBSCR    60
#define DELETE_SUBSCR   61

#define BINARY_LSHIFT   62
#define BINARY_RSHIFT   63
#define BINARY_AND  64
#define BINARY_XOR  65
#define BINARY_OR   66
#define INPLACE_POWER   67
#define GET_ITER    68

#define PRINT_EXPR  70
#define PRINT_ITEM  71
#define PRINT_NEWLINE   72
#define PRINT_ITEM_TO   73
#define PRINT_NEWLINE_TO 74
#define INPLACE_LSHIFT  75
#define INPLACE_RSHIFT  76
#define INPLACE_AND 77
#define INPLACE_XOR 78
#define INPLACE_OR  79
#define BREAK_LOOP  80
#define WITH_CLEANUP    81
#define LOAD_LOCALS 82
#define RETURN_VALUE    83
#define IMPORT_STAR 84
#define EXEC_STMT   85
#define YIELD_VALUE 86
#define POP_BLOCK   87
#define END_FINALLY 88
#define BUILD_CLASS 89

#define HAVE_ARGUMENT   90  /* Opcodes from here have an argument: */

#define STORE_NAME  90  /* Index in name list */
#define DELETE_NAME 91  /* "" */
#define UNPACK_SEQUENCE 92  /* Number of sequence items */
#define FOR_ITER    93

#define STORE_ATTR  95  /* Index in name list */
#define DELETE_ATTR 96  /* "" */
#define STORE_GLOBAL    97  /* "" */
#define DELETE_GLOBAL   98  /* "" */
#define DUP_TOPX    99  /* number of items to duplicate */
#define LOAD_CONST  100 /* Index in const list */
#define LOAD_NAME   101 /* Index in name list */
#define BUILD_TUPLE 102 /* Number of tuple items */
#define BUILD_LIST  103 /* Number of list items */
#define BUILD_MAP   104 /* Always zero for now */
#define LOAD_ATTR   105 /* Index in name list */
#define COMPARE_OP  106 /* Comparison operator */
#define IMPORT_NAME 107 /* Index in name list */
#define IMPORT_FROM 108 /* Index in name list */

#define JUMP_FORWARD    110 /* Number of bytes to skip */
#define JUMP_IF_FALSE   111 /* "" */
#define JUMP_IF_TRUE    112 /* "" */
#define JUMP_ABSOLUTE   113 /* Target byte offset from beginning of code */

#define LOAD_GLOBAL 116 /* Index in name list */

#define CONTINUE_LOOP   119 /* Start of loop (absolute) */
#define SETUP_LOOP  120 /* Target address (relative) */
#define SETUP_EXCEPT    121 /* "" */
#define SETUP_FINALLY   122 /* "" */

#define LOAD_FAST   124 /* Local variable number */
#define STORE_FAST  125 /* Local variable number */
#define DELETE_FAST 126 /* Local variable number */

#define RAISE_VARARGS   130 /* Number of raise arguments (1, 2 or 3) */
/* CALL_FUNCTION_XXX opcodes defined below depend on this definition */
#define CALL_FUNCTION   131 /* #args + (#kwargs<<8) */
#define MAKE_FUNCTION   132 /* #defaults */
#define BUILD_SLICE     133 /* Number of items */

#define MAKE_CLOSURE    134     /* #free vars */
#define LOAD_CLOSURE    135     /* Load free variable from closure */
#define LOAD_DEREF      136     /* Load and dereference from closure cell */ 
#define STORE_DEREF     137     /* Store into cell */ 

/* The next 3 opcodes must be contiguous and satisfy
   (CALL_FUNCTION_VAR - CALL_FUNCTION) & 3 == 1  */
#define CALL_FUNCTION_VAR          140  /* #args + (#kwargs<<8) */
#define CALL_FUNCTION_KW           141  /* #args + (#kwargs<<8) */
#define CALL_FUNCTION_VAR_KW       142  /* #args + (#kwargs<<8) */

/* Support for opargs more than 16 bits long */
#define EXTENDED_ARG  143

解析 pyc

由于包含嵌套 PyCodeObject序六,pyc 中的二進制數(shù)據(jù)實際上是有結(jié)構(gòu)的任连,可以以 XML格式進行解析,從而可視化例诀。使用 pycparser随抠。

而 Python 庫中 dis 的 dis 方法可以對 code對象 進行解析。接收 code對象繁涂,輸出 字節(jié)碼指令信息暮刃。

dis.dis 的輸出:

  • 第一列,是 字節(jié)碼指令 對應(yīng)的 源代碼 在 Python 程序中的行數(shù)
  • 第二列爆土,是當前 字節(jié)碼指令 在 co_code 中的偏移位置
  • 第三列椭懊,當前的字節(jié)碼指令
  • 第四列,當前字節(jié)碼指令的參數(shù)

test.py

import sys

a = 1

def b():
    print a
    a = 2
    print a
>>> source = open('/Users/chao/Desktop/test.py').read()
>>> co = compile(source, 'test.py', 'exec')
>>> import dis
>>> dis.dis(co)
  1           0 LOAD_CONST               0 (-1)
              3 LOAD_CONST               1 (None)
              6 IMPORT_NAME              0 (sys)
              9 STORE_NAME               0 (sys)

  3          12 LOAD_CONST               2 (1)
             15 STORE_NAME               1 (a)

  5          18 LOAD_CONST               3 (<code object b at 0x1005dc930, file "test.py", line 5>)
             21 MAKE_FUNCTION            0
             24 STORE_NAME               2 (b)
             27 LOAD_CONST               1 (None)
             30 RETURN_VALUE
>>> type(co)
<type 'code'>
>>> dir(co)
['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
>>> print co.co_names
('sys', 'a', 'b')
>>> print co.co_name
<module>
>>> print co.co_filename
test.py

參考資料

《Python 源碼剖析》第七章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市氧猬,隨后出現(xiàn)的幾起案子背犯,更是在濱河造成了極大的恐慌,老刑警劉巖盅抚,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件漠魏,死亡現(xiàn)場離奇詭異,居然都是意外死亡妄均,警方通過查閱死者的電腦和手機柱锹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丰包,“玉大人禁熏,你說我怎么就攤上這事∫乇耄” “怎么了瞧毙?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長寄症。 經(jīng)常有香客問我宙彪,道長,這世上最難降的妖魔是什么有巧? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任释漆,我火速辦了婚禮,結(jié)果婚禮上篮迎,老公的妹妹穿的比我還像新娘男图。我一直安慰自己,他們只是感情好柑潦,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著峻凫,像睡著了一般渗鬼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上荧琼,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天譬胎,我揣著相機與錄音,去河邊找鬼命锄。 笑死堰乔,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的脐恩。 我是一名探鬼主播镐侯,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼驶冒!你這毒婦竟也來了苟翻?” 一聲冷哼從身側(cè)響起韵卤,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎崇猫,沒想到半個月后沈条,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡诅炉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年蜡歹,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涕烧。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡月而,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出澈魄,到底是詐尸還是另有隱情景鼠,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布痹扇,位于F島的核電站铛漓,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏鲫构。R本人自食惡果不足惜浓恶,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望结笨。 院中可真熱鬧包晰,春花似錦、人聲如沸炕吸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赫模。三九已至树肃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瀑罗,已是汗流浹背胸嘴。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留斩祭,地道東北人劣像。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像摧玫,于是被迫代替她去往敵國和親耳奕。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容