1. 程式人生 > >python3.6 原始碼分析(五):類的建立

python3.6 原始碼分析(五):類的建立

友情提示:類的建立過程非常複雜, 請自備小本本

位元組碼分析

先來個最簡單的類:

class A:
    pass

編譯一下:

              0 LOAD_BUILD_CLASS
              2 LOAD_CONST               0 (<code object A at 0x00000226D1158ED0, file "", line 1>)
              4 LOAD_CONST               1 ('A')
              6 MAKE_FUNCTION            0
              8
LOAD_CONST 1 ('A') 10 CALL_FUNCTION 2 12 STORE_NAME 0 (A)

可以看到第一條位元組碼就不認識了,好吧,暫時不管他是幹啥的,往下看,接下來三條位元組碼建立了一個函式物件,codeobject正是類的定義體,名字叫A,欸?定義類怎麼變成定義函數了?
好吧,帶著問題繼續看,CALL_FUNCTION,終於呼叫了!等等,CALL_FUNCTION 的引數是2,說明有2個引數,往上數兩個,正好是’A’和剛才建立的函式物件,引數有了,那被呼叫的函式呢,沒了?這是不可能的,唯一的理由就是LOAD_BUILD_CLASS在堆疊中push了一個函式,看看LOAD_BUILD_CLASS的原始碼:

bc = _PyDict_GetItemId(f->f_builtins, &PyId___build_class__);

原來是builtins裡面的一個叫_build_class_的東西,這個東西可能就是開始建立類的第一現場!

build_class

是時候來分析下過程了,全域性搜尋一下build_class,找到了這個一個函式:builtin___build_class__,看樣子應該沒錯了,看下長短,150行。還行:
首先肯定是檢查引數:

if (!PyTuple_Check(args)) {
        PyErr_SetString(PyExc_TypeError,
                        "__build_class__: args is not a tuple"
); return NULL; } nargs = PyTuple_GET_SIZE(args); if (nargs < 2) { PyErr_SetString(PyExc_TypeError, "__build_class__: not enough arguments"); return NULL; } func = PyTuple_GET_ITEM(args, 0); /* Better be callable */ if (!PyFunction_Check(func)) { PyErr_SetString(PyExc_TypeError, "__build_class__: func must be a function"); return NULL; } name = PyTuple_GET_ITEM(args, 1); if (!PyUnicode_Check(name)) { PyErr_SetString(PyExc_TypeError, "__build_class__: name is not a string"); return NULL; }

可以看出來一點有用的資訊:
1. args是tuple,這是cfunction函式呼叫約定,這裡不多說了
2. args至少長度為2,是的,至少得有上面位元組碼裡的類名和functionobject
3. 然後取出類名和functionobject放在func和name變數裡備用

然後獲取基類列表:

bases = PyTuple_GetSlice(args, 2, nargs);

這個很容易理解,前兩個引數剛才已經用了,後面的位置引數肯定就是基類了。
確定元類是一個略微複雜的過程:
如果關鍵字引數為空,也就是metaclass沒有指定:
1. 沒有基類
欽定為type

meta = (PyObject *) (&PyType_Type);
  1. 有基類
    取第一個基類的metaclass
meta = (PyObject *) (base0->ob_type); 

這還沒完呢,取得了暫時的metaclass還要去和基類列表比較,看有沒有衝突之類的,這裡就略過了,不是很重要。。

然後呼叫了meta的_prepare_:

prep = _PyObject_GetAttrId(meta, &PyId___prepare__);
ns = _PyObject_FastCallDict(prep, pargs, 2, mkw);

這個ns預設情況下就是一個空的dict,然而卻很重要,因為他就是我們建立的類的_dict_,所以我們可以通過複寫元類的_prepare_方法改變這個規則,比如改成一個orderdict?
還有最後一個準備工作,那就是求值類體,用上面的ns作為locals,這樣,所有在類中定義的名字都放入了ns

準備工作做完後,要進入正式的類的建立過程了,首先構造引數:

PyObject *margs[3] = {name, bases, ns};

分別是類名,基類列表,dict(看到了吧)。
然後呼叫元類:

cls = _PyObject_FastCallDict(meta, margs, 3, mkw);

很明顯會進入meta的_call_,然而在call中,直接呼叫了_new_:

obj = type->tp_new(type, args, kwds);

所以還是直接看_new_把:
500多行程式碼,列出來沒人看。。。。撿重要的說。
國際慣例,引數驗證:
如果meta就是PyType_Type引數只有一個,那麼就是type(x)這種東西了,很簡單,直接返回x的型別:

 return (PyObject *) Py_TYPE(x);

然後是取引數,驗證metadata,我們就不看了。直接看建立過程把

首先是基類,如果基類tuple為空,則給個PyBaseObject_Type,即object,所有類的基類。
然後處理dict,就是上面的ns,儲存了所有類裡面定義的名字
1. _slots_
第一步是看看類裡面有沒有定義slots,如果不知道slots是什麼,請先看python教程。。。。。如果有,則進行一些名字改造,拼接,一般是拼上一個 _classname

__private =>  _classname__private

然後賦值給ht_slots域。這裡有一點,動態建立的類和內建型別有點不一樣,動態建立的類是PyHeapTypeObject,而內建的類是PyTypeObject。區別就在PyHeapTypeObject後面跟了額外幾個域:

typedef struct _heaptypeobject {
    /* Note: there's a dependency on the order of these members
       in slotptr() in typeobject.c . */
    PyTypeObject ht_type;
    PyAsyncMethods as_async;
    PyNumberMethods as_number;
    PyMappingMethods as_mapping;
    PySequenceMethods as_sequence; /* as_sequence comes after as_mapping,
                                      so that the mapping wins when both
                                      the mapping and the sequence define
                                      a given operator (e.g. __getitem__).
                                      see add_operators() in typeobject.c . */
    PyBufferProcs as_buffer;
    PyObject *ht_name, *ht_slots, *ht_qualname;
    struct _dictkeysobject *ht_cached_keys;
    /* here are optional user slots, followed by the members. */
} PyHeapTypeObject;

就是普通的pytypeobject後面跟了一堆東西。
然後趕緊利用起來:

    type->tp_as_async = &et->as_async;
    type->tp_as_number = &et->as_number;
    type->tp_as_sequence = &et->as_sequence;
    type->tp_as_mapping = &et->as_mapping;
    type->tp_as_buffer = &et->as_buffer;

原來是把內建型別的幾個方法集合放在了結構體後面而已。。。。

然後就是處理一些簡單的屬性比如tp_name什麼的,都是簡單的賦值

最後一個關鍵的地方就是slot的處理,這個slot和_slots_不一樣,這個slot是python內部的東西,它處理了一個方法呼叫的走向。比如繼承自內建type的類,複寫其特殊方法,比如 繼承int,複寫 _add_,那麼當做加法時,_add_會被呼叫,這是怎麼實現的呢?

都在fixup_slot_dispatchers這個函式裡面了。
這個函式遍歷了slotdefs這個靜態陣列,