深入理解 python 虛擬機器:位元組碼靈魂——Code obejct

一無是處的研究僧發表於2023-04-02

深入理解 python 虛擬機器:位元組碼靈魂——Code obejct

在本篇文章當中主要給大家深入介紹在 cpython 當中非常重要的一個資料結構 code object! 在上一篇文章 深入理解 python 虛擬機器:pyc 檔案結構 ,我們簡單介紹了一下在 code object 當中有哪些欄位以及這些欄位的簡單含義,在本篇文章當中將會舉一些例子以便更加深入理解這些欄位。

Code 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 */
    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) and co_firstlineno (used only in
       comparisons).  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.
    */
    unsigned char *co_cell2arg; /* Maps cell vars which are arguments. */
    PyObject *co_filename;	/* unicode (where it was loaded from) */
    PyObject *co_name;		/* unicode (name, for reference) */
    int co_firstlineno;		/* first source line number */
    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 */
} PyCodeObject;

下面是 code object 當中各個欄位的作用:

  • 首先需要了解一下程式碼塊這個概念,所謂程式碼塊就是一個小的 python 程式碼,被當做一個小的單元整體執行。在 python 當中常見的程式碼塊塊有:函式體、類的定義、一個模組。

  • argcount,這個表示一個程式碼塊的引數個數,這個引數只對函式體程式碼塊有用,因為函式可能會有引數,比如上面的 pycdemo.py 是一個模組而不是一個函式,因此這個引數對應的值為 0 。

  • co_code,這個物件的具體內容就是一個位元組序列,儲存真實的 python 位元組碼,主要是用於 python 虛擬機器執行的,在本篇文章當中暫時不詳細分析。

  • co_consts,這個欄位是一個列表型別的欄位,主要是包含一些字串常量和數值常量,比如上面的 "__main__" 和 100 。

  • co_filename,這個欄位的含義就是對應的原始檔的檔名。

  • co_firstlineno,這個欄位的含義為在 python 原始檔當中第一行程式碼出現的行數,這個欄位在進行除錯的時候非常重要。

  • co_flags,這個欄位的主要含義就是標識這個 code object 的型別。0x0080 表示這個 block 是一個協程,0x0010 表示這個 code object 是巢狀的等等。

  • co_lnotab,這個欄位的含義主要是用於計算每個位元組碼指令對應的原始碼行數。

  • co_varnames,這個欄位的主要含義是表示在一個 code object 本地定義的一個名字。

  • co_names,和 co_varnames 相反,表示非本地定義但是在 code object 當中使用的名字。

  • co_nlocals,這個欄位表示在一個 code object 當中本地使用的變數個數。

  • co_stackszie,因為 python 虛擬機器是一個棧式計算機,這個引數的值表示這個棧需要的最大的值。

  • co_cellvars,co_freevars,這兩個欄位主要和巢狀函式和函式閉包有關,我們在後續的文章當中將詳細解釋這個欄位。

CodeObject 詳細分析

現在我們使用一些實際的例子來分析具體的 code object 。

import dis
import binascii
import types

d = 10


def test_co01(c):
    a = 1
    b = 2
    return a + b + c + d

在前面的文章當中我們提到過一個函式是包括一個 code object 物件,test_co01 的 code object 物件的輸出結果(完整程式碼見co01)如下所示:

code
   argcount 1
   nlocals 3
   stacksize 2
   flags 0043 0x43
   code b'6401007d01006402007d02007c01007c0200177c0000177400001753'
  9           0 LOAD_CONST               1 (1)
              3 STORE_FAST               1 (a)

 10           6 LOAD_CONST               2 (2)
              9 STORE_FAST               2 (b)

 11          12 LOAD_FAST                1 (a)
             15 LOAD_FAST                2 (b)
             18 BINARY_ADD
             19 LOAD_FAST                0 (c)
             22 BINARY_ADD
             23 LOAD_GLOBAL              0 (d)
             26 BINARY_ADD
             27 RETURN_VALUE
   consts
      None
      1
      2
   names ('d',)
   varnames ('c', 'a', 'b')
   freevars ()
   cellvars ()
   filename '/tmp/pycharm_project_396/co01.py'
   name 'test_co01'
   firstlineno 8
   lnotab b'000106010601'
  • 欄位 argcount 的值等於 1,說明函式有一個引數,這個函式 test_co01 有一個引數 c 是相互對應的。
  • 欄位 nlocals 的值等於 3,說明在函式 test_co01 當中一個一共實現了三個函式本地變數 a, b, c 。
  • 欄位 names,對應程式碼程式碼當中的 co_names,根據前面的定義就是 d 這個全域性變數在函式 test_co01 當中使用,但是卻沒有在函式當中定義了。
  • 欄位 varnames,這個就表示在本地定義使用的變數了,在函式 test_co01 當中主要有三個變數 a, b, c 。
  • 欄位 filename,就是 python 檔案的地址了。
  • 欄位 firstlineno 說明函式的第一行出現在對應 python 程式碼的 第 8 行。

Flags 欄位詳細分析

我們具體使用 python3.5 的原始碼進行分析,在 cpython 虛擬機器的具體實現如下所示(Include/code.h):

/* Masks for co_flags above */
#define CO_OPTIMIZED	0x0001
#define CO_NEWLOCALS	0x0002
#define CO_VARARGS	0x0004
#define CO_VARKEYWORDS	0x0008
#define CO_NESTED       0x0010
#define CO_GENERATOR    0x0020
/* The CO_NOFREE flag is set if there are no free or cell variables.
   This information is redundant, but it allows a single flag test
   to determine whether there is any extra work to be done when the
   call frame it setup.
*/
#define CO_NOFREE       0x0040

/* The CO_COROUTINE flag is set for coroutine functions (defined with
   ``async def`` keywords) */
#define CO_COROUTINE            0x0080
#define CO_ITERABLE_COROUTINE   0x0100

如果 flags 欄位和上面的各個宏定義進行 & 運算,如果得到的結果大於 0,則說明符合對應的條件。

上面的宏定義的含義如下所示:

  • CO_OPTIMIZED,這個欄位表示 code object 是被最佳化過的,使用函式本地定義的變數。

  • CO_NEWLOCALS,這個欄位的含義為當這個 code object 的程式碼被執行的時候會給棧幀當中的 f_locals 物件建立一個 dict 物件。

  • CO_VARARGS,表示這個 code object 物件是否含有位置引數。

  • CO_VARKEYWORDS,表示這個 code object 是否含有關鍵字引數。

  • CO_NESTED,表示這個 code object 是一個巢狀函式。

  • CO_GENERATOR,表示這個 code object 是一個生成器。

  • CO_COROUTINE,表示這個 code object 是一個協程函式。

  • CO_ITERABLE_COROUTINE,表示 code object 是一個可迭代的協程函式。

  • CO_NOFREE,這個表示沒有 freevars 和 cellvars,即沒有函式閉包。

現在再分析一下前面的函式 test_co01 的 flags,他對應的值等於 0x43,則說明這個函式滿足三個特性分別是 CO_NEWLOCALS,CO_OPTIMIZED 和 CO_NOFREE。

freevars & cellvars

我們使用下面的函式來對這兩個欄位進行分析:

def test_co02():
    a = 1
    b = 2

    def g():
        return a + b
    return a + b + g()

上面的函式的資訊如下所示(完整程式碼見co02):

code
   argcount 0
   nlocals 1
   stacksize 3
   flags 0003 0x3
   code
      b'640100890000640200890100870000870100660200640300640400860000'
      b'7d0000880000880100177c00008300001753'
 15           0 LOAD_CONST               1 (1)
              3 STORE_DEREF              0 (a)

 16           6 LOAD_CONST               2 (2)
              9 STORE_DEREF              1 (b)

 18          12 LOAD_CLOSURE             0 (a)
             15 LOAD_CLOSURE             1 (b)
             18 BUILD_TUPLE              2
             21 LOAD_CONST               3 (<code object g at 0x7f133ff496f0, file "/tmp/pycharm_project_396/co01.py", line 18>)
             24 LOAD_CONST               4 ('test_co02.<locals>.g')
             27 MAKE_CLOSURE             0
             30 STORE_FAST               0 (g)

 20          33 LOAD_DEREF               0 (a)
             36 LOAD_DEREF               1 (b)
             39 BINARY_ADD
             40 LOAD_FAST                0 (g)
             43 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
             46 BINARY_ADD
             47 RETURN_VALUE
   consts
      None
      1
      2
      code
         argcount 0
         nlocals 0
         stacksize 2
         flags 0013 0x13
         code b'8800008801001753'
 19           0 LOAD_DEREF               0 (a)
              3 LOAD_DEREF               1 (b)
              6 BINARY_ADD
              7 RETURN_VALUE
         consts
            None
         names ()
         varnames ()
         freevars ('a', 'b')
         cellvars ()
         filename '/tmp/pycharm_project_396/co01.py'
         name 'g'
         firstlineno 18
         lnotab b'0001'
      'test_co02.<locals>.g'
   names ()
   varnames ('g',)
   freevars ()
   cellvars ('a', 'b')
   filename '/tmp/pycharm_project_396/co01.py'
   name 'test_co02'
   firstlineno 14
   lnotab b'0001060106021502'

從上面的輸出我們可以看到的是,函式 test_co02 的 cellvars 為 ('a', 'b'),函式 g 的 freevars 為 ('a', 'b'),cellvars 表示在其他函式當中會使用本地定義的變數,freevars 表示本地會使用其他函式定義的變數。

再來分析一下函式 test_co02 的 flags,他的 flags 等於 0x3 因為有閉包的存在因此 flags 不會存在 CO_NOFREE,也就是少了值 0x0040 。

stacksize

這個欄位儲存的是在函式在被虛擬機器執行的時候所需要的最大的棧空間的大小,這也是一種最佳化手段,因為在知道所需要的最大的棧空間,所以可以在函式執行的時候直接分配指定大小的空間不需要在函式執行的時候再去重新擴容。

def test_stack():
    a = 1
    b = 2
    return a + b

上面的程式碼相關位元組碼等資訊如下所示:

code
   argcount 0
   nlocals 2
   stacksize 2
   flags 0043 0x43
   code b'6401007d00006402007d01007c00007c01001753'
   #					  位元組碼指令		 # 位元組碼指令引數 # 引數對應的值
 24           0 LOAD_CONST               1 (1)
              3 STORE_FAST               0 (a)

 25           6 LOAD_CONST               2 (2)
              9 STORE_FAST               1 (b)

 26          12 LOAD_FAST                0 (a)
             15 LOAD_FAST                1 (b)
             18 BINARY_ADD
             19 RETURN_VALUE
   consts
      None # 下標等於 0 的常量
      1 	 # 下標等於 1 的常量
      2		 # 下標等於 2 的常量
   names ()
   varnames ('a', 'b')
   freevars ()
   cellvars ()

我們現在來模擬一下執行過程,在模擬之前我們首先來了解一下上面幾條位元組碼的作用:

  • LOAD_CONST,將常量表當中的下標等於 i 個物件載入到棧當中,對應上面的程式碼 LOAD_CONST 的引數 i = 1。因此載入測常量等於 1 。因此現在棧空間如下所示:

  • STORE_FAST,將棧頂元素彈出並且儲存到 co_varnames 對應的下標當中,根據上面的位元組碼引數等於 0 ,因此將 1 儲存到 co_varnames[0] 對應的物件當中。

  • LOAD_CONST,將下標等於 2 的常量載入進入棧中。

  • STORE_FAST,將棧頂元素彈出,並且儲存到 varnames 下標為 1 的物件。

  • LOAD_FAST,是取出 co_varnames 對應下標的資料,並且將其壓入棧中。我們直接連續執行兩個 LOAD_FAST 之後棧空間的佈局如下:

  • BINARY_ADD,這個位元組碼指令是將棧空間的兩個棧頂元素彈出,然後將兩個資料進行相加操作,然後將相加得到的結果重新壓入棧中。

  • RETURN_VALUE,將棧頂元素彈出並且作為返回值返回。

從上面的整個執行過程來看整個棧空間使用的最大的空間長度為 2 ,因此 stacksize = 2 。

總結

在本篇文章當中主要分析了一些 code obejct 當中比較重要的欄位,code object 是 cpython 虛擬機器當中一個比較重要的資料結構,深入的去理解這裡面的欄位對於我們理解 python 虛擬機器非常有幫助。


本篇文章是深入理解 python 虛擬機器系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

更多精彩內容合集可訪問專案:https://github.com/Chang-LeHung/CSCore

關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演算法與資料結構)知識。

相關文章