我認識的python(5)

weixin_33797791發表於2018-07-14

直譯器篇

python的虛擬機器其實本質上就是模擬cpu執行過程,對應函式棧幀實現Python位元組碼的執行。
當虛擬機器真正執行的時候,其面對的不是PyCodeObject而是P yPyFrameObject

typedef struct _frame{
    PyObject_VAR_HEAD
    struct _frame *f_back;
    PyCodeObject *f_code;
    PyObject *f_builtins;
    PyObject *f_globals;
    PyObject *f_locals;
    PyObject **f_valuestack;
    PyObject **f_stacktop;
    ....
    int f_lasti;
    int f_lineno;
    ....
    //動態記憶體,維護(區域性變數+cell物件集合+free物件集合+執行時棧)所需要的空間
    PyObject *f_localsplus[1];
}
複製程式碼

執行時候的狀態

我認識的python(5)

建立PyFrameObject過程

PyFrameObject *PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals, PyObject *locals){
    PyFrameObject *f;
    Py_ssize_t extras, ncells, nfree, i;
    ncells = PyTuple_GET_SIZE(code->co_cellvals);
    nfrees = PyTuple_GET_SIZE(code->co_freevars);
    
    extras = code->code_stack + code->co_nlocals + ncells + nfrees;
    f = PyObject_GC_NewVar(PyFrameObject, &PyFrame_Type, extras);
    
    extras = code->con_nlocals + ncells + nfrees;
    f->f_valuestack = f->f_localsplus + extras;
    f->f_stacktop = f->f_valuestack;
    return f;
}
複製程式碼

我認識的python(5)

作用域

LEGB
一般分為兩種引用方式,屬性引用和名字引用,屬性引用本質是到名字引用空間中查詢一個名字所引用的物件。

直譯器執行位元組碼核心程式碼

PyObject *PyEval_EvalFrameEx(PyFrameObject *f, int throwflag){
    ...
    why = WHY_NOT;
    ...
    for(;;){
        ...
        fast_next_opcode:
            f->f_lasti = INSTR_OFFSET();  //類似PC暫存器的作用
            opcode = NEXTOP();
            oparg = 0;
            if(HAS_ARG(opcode))
                oparg = NEXTARG();
        dispatch_opcode:
            switch(opcode){
                case NOP:
                    goto fast_next_opcode;
                case LOAD_FAST:
                    ...
            }
    }
    
}

why表明一個位元組碼指令執行結果。
enum Why_Code {
    WHY_NOT = 0x0001
    WHY_EXCEPTION = 0x0002 
    WHY_RERAISE = 0x0004 //在finally中觸發異常
    WHY_RETURN = 0x0008
    WHY_BREAK = 0x0010
    WHY_CONTINUE = 0x0020
    WHY_YIELD = 0x0040
    
}
複製程式碼

執行緒程式概念

程式在執行模式分為程式或者執行緒。這裡就會引入一個概念,PyFrameObject本質上是屬於某程式或者執行緒(當然最終肯定是與執行緒關聯)。程式是一個資源的管理單元,執行緒是一個程式最小執行單元。多個執行緒共享程式地址空間的全域性資訊。

Cpu切換任務的時候必須儲存執行緒的執行環境,類似作業系統程式排程。在python中一個執行緒擁有一個PyThreadState物件,而PyInterpreterState物件代表程式狀態物件。

為了節省記憶體,cpython在實現中讓多個執行緒共享相同的Module物件。一般來說一個python程式擁有一個interpreter物件,一個interpreter物件維護多個PyThreadState,多個Pythreadstate輪流使用位元組碼直譯器。

typedef struct _is {
    struct _is *next;
    struct _ts *tstate_head;
    
    PyObject *modules;
    PyObject *sysdict;
    PyObject *builtins;
    ...
} PyInterpreterState

typedef struct _ts {
    struct _ts *next;
    PyInterpreterState *interp;
    struct _frame *frame;
    int recursion_depth;
    ...
    PyObject *dict;
    ...
    long thread_id;
} PyThreadState;
複製程式碼

當python虛擬機器開始執行的時候,會從frame中取出PyFrameObject設定為當前執行環境,建立新的棧幀則重新設定關係。

我認識的python(5)

位元組碼與PyFrameObject

比如我們經常用到賦值語句: i = 1,其對應的位元組碼為:
0 LOAD_CONST 0 (1) 3 STORE_NAME 0 (i)

+對應的位元組碼

BINARY_ADD
    w = POP();
    v = TOP();
    if(PyInt_CheckExact(v) && PyInt_CheckExact(w)){
        register long a, b, i;
        a = PyInt_AS_LONG(v);
        b = PyInt_AS_LONG(w);
        i = a+b;
        if((i^a) <0 && (i^b)<0)
            goto slow_add;
        x = PyInt_FromLong(i);
    }
    else if (PyString_CheckExact(v) && PyString_CheckExact(w)){
        x = string_concatenate(v, w, f, next_instr);
        goto skip_decref_vx;
    }
    else{
        slow_add:
            x = PyNumber_Add(v, w);
    }
    Py_DECREF(v);
skip_decref_vx:
    Py_DECREF(w);
    SET_TOP(x);
    break;
複製程式碼

邏輯語句的實現

if語句的邏輯比較簡單,類似彙編指令裡面的jump系列的命令,根據絕對地址或者相對地址找到下一條位元組碼指令。
for語句會涉及一個特別的位元組碼SETUP_LOOP

typedef struct _frame{
    ....
    int f_iblock;
    PyTryBlock f_blockstack[CO_MAXBLOCKS];
}

void PyFrame_BlockSetup(PyFrameObject *f, int type, int handler, int level){
    PyTryBlock *b;
    b = &f->f_blockstack[f->f_iblock++];
    b->b_type = type;
    b->b_level = level;
    b->b_handler = handler
}
複製程式碼

我認識的python(5)

我認識的python(5)

函式物件

函式物件是在執行位元組碼的過程生成出來,其本質是包含了作用域和code物件的一個結構。

typedef struct {
    PyObject_HEAD
    PyObject *func_code;
    PyObject *func_globals;
    PyObject *func_defaults;
    PyObject *func_closure;
    PyObject *func_doc;
    PyObject *func_name;
    PyObject *func_dict;
    PyObject *func_weakreflist;
    PyObject *func_module;
} PyFunctionObj
複製程式碼

PyFunctionObject是python程式碼在執行時動態產生的,靜態資訊儲存在func_code之中。 對於一段程式碼而言,PyCodeObject物件只有一個,但PyFunctionObject可以有多個。

我認識的python(5)
載入const中的code物件,然後建立一個function物件

MAKE_FUNCTION邏輯
PyObject* PyFunction_New(PyObject *code, PyObject *globals){
    PyFunctionObject *op = PyObject_GC_New(PyFunctionObject, &PyFunction_Type);
    static PyObject *__name__ = 0;
    if(op!=NULL){
        ...
        op->func_code = code;
        op->func_globals = globals;
        op->func_name = ((PyCodeObject *)code)->co_name;
        consts = ((PyCodeObject *)code)->co_consts;
        if(PyTuple_size(consts)>=1) {
            doc = PyTuple_GetItem(consts, 0);
            if(!PyString_Check(doc) && !PyUnicode_Check(doc))
                doc = Py_None
        }
        else 
            doc = Py_None
        ...
        }
        ...
    }
} 

不論CFunction和Method呼叫都會進入這個call_function

static PyObject* call_function(PyObject ***pp_stack, int oparg){
    int na = oparg & 0xff;
    int nk = (oparg>>8) & 0xff;
    int n = na + 2 * nk; //需要讀取的引數個數
    
    PyObject **pfunc = (*pp_stack) -n -1;
    PyObject *func = *pfunc;
    PyObject *x, *w;
    
    if(PyCFunction_Check(func) && nk == 0){
        ...
    } else {
        if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL){
            ...
        }
        if(PyFunction_Check(func))
            x = fast_function(func, pp_stack, n, na, nk);
        else
            x = do_call(func, pp_stack, na, nk);
        ...
    }
}
...
return x;

fast_function裡面的過程大致就是建立棧幀,然後從ThreadState中取出棧幀新增並呼叫執行新的棧幀
而且新棧幀的globals來源於之前棧幀的f_globals以及f_locals

函式的區域性變數是存放在PyFrameObject的 *f_localsplus對應的空間裡面,通過load_fast和store_fast進行操作
複製程式碼

我認識的python(5)

建立一個類A對應的位元組碼

class A(object):
    def __init__(self):
        c = 10
        print c

    def b(self):
	print "22"
複製程式碼

我認識的python(5)

類以及型別

我認識的python(5)

我認識的python(5)

在python中每個物件都有一個type物件,可以通過物件的__class__獲取,對於instance的type對應是class物件,而對於class物件的type對應的就是metaclass物件。這個物件在python中對應的就是PyType_Type。

python中每個class物件直接或者間接會與object物件存在is_kind_of關係,type object對應的是PyObject_Type

對於int型別,其找到+對應的方法如下所示:
從PyInt_Type中找到符號表對應的__add__,對應找到函式簇的nb_add。

我認識的python(5)
對於PyInt_Type中tp_dict的填充是在python啟動後初始化過程中會動態地給內建型別對應的PyTypeObject中填充東西,其中就包括了tp_dict。

<type 'type'>對應是的PyType_Type

class ==> PyTypeObject
建立類的過程,為class設定基類關係,然後為其設定型別type,然後初始化dict內容

int PyType_Ready(PyTypeObject *type)
{
    PyObject *dict, *bases;
    PyTypeObject *base;
    Py_ssize_t i, n;
    
    //假設父類不存在以及非PyBaseObject_Type。所以type的父類為object
    base = type->tp_base;
    if(base == NULL && type != &PyBaseObject_Type){
        base = type->tp_base = &PyBaseObject_Type;
    }
    
    //該條件判斷物件是否初始化完畢
    if(base && base->tp_dict == NULL){
        PyType_Ready(base)
    }
    
    //拷貝父類的型別物件賦值
    if (type->ob_type == NULL && base != NULL){
        type->ob_type = base->ob_type;
        ......
        
    }
    
}
複製程式碼

我認識的python(5)

這一步是進行tp_dict的填充

我認識的python(5)

為pylist_Type填充__getitem__

我認識的python(5)

建立類的位元組碼分析

比如某個類的定義如下

class A(object):

    def __init__(self):
        pass
    
    def f(self):
        pass
        
  1           0 LOAD_CONST               0 ('A')
              3 LOAD_NAME                0 (object)
              6 BUILD_TUPLE              1
              9 LOAD_CONST               1 (<code object A at 0x1073f73b0, file "test.py", line 1>)
             12 MAKE_FUNCTION            0
             15 CALL_FUNCTION            0
             18 BUILD_CLASS         
             19 STORE_NAME               1 (A)
             22 LOAD_CONST               2 (None)
             25 RETURN_VALUE 
複製程式碼

大致的邏輯是載入A,object建立關係繼承列表,載入code物件,建立一個function,然後呼叫function。function對應的內容其實就是函式code的內容。

python引擎在執行call_function其實對應是執行兩次def函式,建立兩個FunctionObject物件。

我認識的python(5)
之後一個類的建立條件則滿足了,呼叫build_class建立一個class物件 build_class邏輯

PyObject * build_class(PyObject *methods, PyObject *bases, PyObject *name){
    PyObject *metaclass = NULL, *result, *base;
    if(PyDict_check(methods))
        metaclass = PyDict_GetItemString(methods, "__metaclass__");
    if (metaclass != NULL)
        Py_INCRE(metaclass);
    else if (PyTuple_checks(bases) && PyTuple_Get_Size(bases)>0){
        base = PyTuple_Get_ITEM(bases, 0);
        metaclass = PyObject_GetAttrString(bases, "__class__");
    }else{
        ....
    }
    result = PyObject_CallFunctionObjArgs(metaclass, name, bases, methods, NULL);
    .....
    return result;
    
}

檢視元類是否定義,沒有則從父類第一個提取出__class__作為metaclass。
複製程式碼

類和instance的__new__的差別

我認識的python(5)

python的instance和class都擁有__dict__,因此不同物件可以設定不同的內容,不同的calss可以設定不同的內容。

相關文章