《Python 原始碼剖析》一些理解以及勘誤筆記(1)

s1mba發表於2015-04-26

以下是本人閱讀此書時理解的一些筆記,包含一些影響文義的筆誤修正,當然不一定正確,貼出來一起討論。

注:此書剖析的原始碼是2.5版本,在python.org 可以找到原始碼。紙質書閱讀,pdf 貼圖。

文章篇幅太長,故切分成3部分,這是第一部分。


p9:int_repr 函式中 PyObject_Print(str, stdout, 0);    stdout 修改為 out


p23 & p263:tp_as_number.nb_add 修改為  tp_as_number->nb_add


p23 & p271:tp_as_mapping.mp_subscript 修改為 tp_as_mapping->mp_subscript 

tp_as_sequence.sq_item 修改為 tp_as_sequence->sq_item


p25:執行時整數物件及其型別之間的關係理解


 對於int(10) 的 ob_refcnt 來說可以理解為多個ref 引用了這個物件,ob_type 是指向其型別物件的指標,ob_ival 是具體數值。

int(10) 是PyIntObject 的例項物件,比PyObject 多一個ob_ival 成員,PyVarObject 比PyObject 多一個int ob_size; 即表示元素個數,當

然具體的 如 PyStringObject 還會有其他的成員,如下所示。可以認為 PyObject 是 整數型別等定長物件的頭, PyVarObject 是 str 等

定長物件的頭,如下所示,注意下面帶EXTRA 字樣的巨集 只有在debug 模式下才存在,故可以忽略不計。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD           \
    _PyObject_HEAD_EXTRA        \
    Py_ssize_t ob_refcnt;       \
    struct _typeobject *ob_type;

/* PyObject_VAR_HEAD defines the initial segment of all variable-size
 * container objects.  These end with a declaration of an array with 1
 * element, but enough space is malloc'ed so that the array actually
 * has room for ob_size elements.  Note that ob_size is an element count,
 * not necessarily a byte count.
 */

#define PyObject_VAR_HEAD       \
    PyObject_HEAD           \
    Py_ssize_t ob_size; /* Number of items in variable part */

/* Nothing is actually declared to be a PyObject, but every pointer to
 * a Python object can be cast to a PyObject*.  This is inheritance built
 * by hand.  Similarly every pointer to a variable-size Python object can,
 * in addition, be cast to PyVarObject*.
 */

typedef struct _object
{
    PyObject_HEAD
} PyObject;

typedef struct
{
    PyObject_VAR_HEAD
} PyVarObject;

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct {
    PyObject_VAR_HEAD
    long ob_shash;
    int ob_sstate;
    char ob_sval[1];

    /* Invariants:
     *     ob_sval contains space for 'ob_size+1' elements.
     *     ob_sval[ob_size] == 0.
     *     ob_shash is the hash of the string or -1 if not computed yet.
     *     ob_sstate != 0 if the string object is in stringobject.c's
     *       'interned' dictionary; in this case the two references
     *       from 'interned' to this object are *not counted* in ob_refcnt.
     */

} PyStringObject;

PyInt_Type、PyBaseObject_Type、PyType_Type 都是PyTypeObject 的例項物件。PyInt_Type 的 tp_base 指向其基類對

象 PyBaseObject_Type,而他們的 ob_type 都指向 PyType_Type,假設定義 class A(object):pass 那麼 A.__class__ 、 

int.__class__、 type.__class__ 、object.__class__ 都是 <type 'type'> 即 metaclass;但 A.__bases__、int.__bases__、type.__bases__ 都是 

<type 'object'> ,bool.__bases__ 為<type 'int'> ,object.__bases__ 為 ()。

Python 2.2 之前的內建型別不能被繼承的原因就在於沒有在 type 中尋找某個屬性的機制。


需要注意的是型別物件是超越引用計數規則的,每一個物件指向型別物件的指標不被視為對型別物件的引用,即型別物件永遠不會

被析構。當然一個對象被析構也不一定馬上釋放記憶體,往往都是大量採用記憶體物件池技術(要麼預先分配,要麼將銷燬的物件新增進池),避免頻繁地

申請和釋放記憶體。比如位於[-5,257)之間 的小整數快取在小整型物件池中,a=256, b=256, a is b 是 True 的,也就是引用著同個物件。短字串同理,注意字串效能相關的 '+' 操作和 join 操作:每次 '+' 操作都需要新建立物件,效能較差;join 先計算結果物件的總長度,建立一個結果字串物件,然後拷貝資料到結果記憶體位置,所以效能較好。


如下的PyInt_Type 的初始化程式碼,可知初始化 ob_refcnt 為1,而 ob_type 為 PyType_Type 的地址;PyType_Type 的初始化也是類似

的,故其 ob_type 指向自身,但它的 tp_name 為 ”type" 。 tp_basicsize 和 tp_itemsize 分別表示物件基本大小和元素大小,對於

PyIntObject 來說沒有元素大小,如果是str 即 PyVarObject,tp_itemsize 即 sizeof(char),加上一個 ob_size 引數,也能在建立 instance 物件時確定

分配的記憶體大小,即 /* For allocation */

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
#define PyObject_HEAD_INIT(type)    \
    _PyObject_EXTRA_INIT        \
    1, type,
    
PyTypeObject PyInt_Type = {
    PyObject_HEAD_INIT(&PyType_Type)
    0,  /* ob_size */
    "int",  /* tp_name */
    sizeof(PyIntObject), /* tp_basicsize */
    0,  /* tp_itemsize */
    ...,
    };

Python 物件的多型性正是利用函式傳遞PyObject* 而不是具體的PyIntObject* 等來實現,具體判斷是什麼型別物件是通過動態判斷對

象的 ob_type 來實現,考慮如下的程式碼:

 C++ Code 
1
2
3
4
void Print(PyObject* object)
{
    object->ob_type->tp_print(object);
}

如果傳遞給 Print 函式的是 PyIntObject* ,那呼叫的是 PyIntObject 物件對應的輸出操作,如果是PyStringObject* 同理。


p87: 倒數第二段尾句,如果被設定,則不會返回Dummy 態 entry。Dummy 修改為 Unused


p98: 程式碼中的一個for 迴圈可以不需要 PyObject* value = entry->me_value; 第二個for 迴圈可以不需要 PyObject* key = entry->me_key;

第二個 for 迴圈列印應該是 (value->ob_type)->tp_print(value, stdout, 0);


p101: 模擬實現的Small Python 並沒有貼出完整程式碼,順著作者思路寫完了,程式碼在 https://github.com/JnuSimba/Small_Python 


p115: 在Python 中類、函式、module 都對應著一個獨立的名字空間PyDictObject,因此都會有一個PyCodeObject 物件(code  block)與其對應(對

應一個PyFrameObject),可以通過 __code__ 訪問到,PyCodeObject 物件以巢狀遞迴(裡面的PyCodeObject 物件儲存在外面物件的

co_consts 裡)的方式寫入pyc 檔案,括co_code 指向的PyStringObject 物件即位元組碼指令,最終寫入檔案的是string or number實際上整個位元組

碼指令序列就是一個在C中普通的字元組,只不過每個指令(100來個,opcode.h 中巨集定義為一個具體數值)有預定義的含義,在 interpreter main 

loop 中不斷取出每條指令,進而 switch case 進行指令實現(即Python 直譯器 C 原始碼實現)。如下使用 dis.dis 展示的位元組碼指令:

1    0 LOAD_CONST 0 (1)

      3 STORE_NAME 0 (i)

最左面第一列表示位元組碼指令對應的原始碼在py 檔案的行數,左起第二列是當前位元組碼指令在co_code 的偏移位置,第三列顯示當前位元組碼指令,最後

一列是指令引數(括號內是類似指令提示的東東)。比如 LOAD_CONST 0 所做的操作就是從 f->f_code->co_consts 常量表(PyTupleObject)中取出

序號為0的元素即整數物件1,將其壓入虛擬機器的執行時棧中;STORE_NAME 0 先從符號表 f->f_code->co_names(PyTupleObject)獲取序號為0

的元素的作為變數名,將前面獲取到的整數對象從棧中pop 出作為變數值,將(i, 1)新增到 f->f_locals 中。以上可以認為是位元組碼執行的縮影,即不

斷在執行時棧和名字空間內進行運算。

注:一個位元組碼指令1個位元組,一個指令引數2個位元組,第二列的偏移值就是這麼計算得來的。


p137: 這個棧空間的大小儲存在 f_stacksize 中 ...  f_stacksize 修改為 co_stacksize 即是PyCodeObject 的成員。

Python 執行的某個時刻的執行時環境如下圖所示:

p115 條目所說,PyFrameObject 的f_locals、f_globals、f_builtins 分別指向不同的名字空間,對於類or 函式的 f_locals 和 f_globals

指向往往是不一樣的(實際上函式的 f_locals = NULL),但module 的local 和global 指向是一樣的,即全域性名字空間,當然三者的global 指向肯定是一

致的,Python 所有執行緒都共享同樣的 builtin 名字空間(也就是 __builtin__ module 所維護的dict)。f_code 指向對應的PyCodeObject 物件,

Frame 的呼叫鏈用f_back 連線起來。注意:f_locals 和 f_globals 都可能在執行時動態新增刪除條目,假設函式g 定義在 f 之後,在執行 f() 時 函式對

象g 已經被建立產生並且被加入到 f_globals 中,於是可以在 f 中呼叫 g,這點與c 語言不同(基於函式出現位置)。





p140: 關於名字、作用域和名字空間。

關鍵詞:靜態作用域、LEGB(local, enclosing 閉包, global, builtin)查詢原則(注意在module 內查詢)、最內巢狀作用域原則

最內巢狀作用域原則:由一個賦值語句引進的名字在這個賦值語句所在的作用域裡是可見的,而且在其內部巢狀的每個作用域

內裡也可見,除非它被巢狀於內部的,引進同樣名字的另一條賦值語句所遮蔽

global 表示式不受LEGB 原則約束;名字引用受LEGB原則約束;屬性引用不受約束,可以簡單理解為帶 .的表示式,比如引用其

他模組的函式或變數 or 類的成員函式或class 變數引用。


p180: [COMPARE_OP]  程式碼段中第二個if 判斷應該是JUMP_IF_TRUE


p185: PyFrameObject 中的 PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */ ,PyTryBlock 在迴圈控制流和異

常處理流中被用於儲存虛擬機器進入流程前的一些狀態資訊,以便恢復到先前狀態。

 C++ Code 
1
2
3
4
5
typedef struct {
    int b_type;         /* what kind of block this is */
    int b_handler;      /* where to jump to find handler */
    int b_level;        /* value stack level to pop to */
} PyTryBlock;

p193: while_control.py 的位元組碼中 a 修改為 i


p201: PyThreadState 物件是Python 為執行緒準備的在Python 虛擬機器一級儲存執行緒狀態資訊的物件,比如異常資訊存放在 curexc_type、curexc_value 

,通過 sys.exc_info()[0/1] 可以獲取。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef struct _ts {
    /* See Python/ceval.c for comments explaining most fields */
    
    struct _ts *next;
    PyInterpreterState *interp;

    struct _frame *frame;
    int recursion_depth;
    /* 'tracing' keeps track of the execution depth when tracing/profiling.
       This is to prevent the actual trace/profile code from being recorded in
       the trace/profile. */

    ...
    PyObject *curexc_type;
    PyObject *curexc_value;
    PyObject *curexc_traceback;

    PyObject *exc_type;
    PyObject *exc_value;
    PyObject *exc_traceback;

    PyObject *dict;  /* Stores per-thread state */
    ...

    PyObject *async_exc; /* Asynchronous exception to raise */
    long thread_id; /* Thread id where this tstate was created */

} PyThreadState;

p206: 異常產生的處理流程:

假如h() 函式內執行了一個 1/0 操作,那麼在PyEval_EvalFrameEx() (注意此函式是被遞迴呼叫的,可以認為一個PyFrameObject 對

應一個函式)內switch 執行此位元組碼指令後發現產生產生除數為0的異常,便首先記錄異常資訊到 PyThreadState.curexc_type = 

&ZeroDivisionError;  PyThreadState.curexc_value = &"integer divison or modulo by zero";  接著由指令執行結果返回NULL(進而why 

== WHY_EXCEPTION) 得知產生異常,先建立 traceback 物件,進而在當前棧幀尋找 except 語句,以尋找開發人員指定的捕捉異

的東西,如果沒有找到,那麼Python 虛擬機器將退出當前的活動棧幀,並沿著棧幀連結串列向上回退到上一個棧幀(tstate->frame = f-

>f_back),這個沿著棧幀鏈不斷回退的過程稱之為棧幀展開,在展開的過程中,Python 虛擬機器不斷建立與各個棧幀對應的 

traceback 物件,並將其連結成連結串列,如下圖所示,注意,tstate->curexc_traceback 指向最新的 traceback 物件。如果沒有在任何一


層設定異常捕捉程式碼,那麼最後Python 虛擬機器從執行緒狀態物件中取出其維護的 traceback 物件,並遍歷 traceback 物件鏈表,逐個輸


出其中的資訊,也就是我們所熟悉的 Traceback (most recent last call): ...




 C++ Code 
1
2
3
4
5
6
7
typedef struct _traceback {
    PyObject_HEAD
    struct _traceback *tb_next;
    struct _frame *tb_frame;
    int tb_lasti; //發生異常時執行的最後一行指令
    int tb_lineno; // 發生異常時指令對應的原始碼行
} PyTracebackObject;

那如果程式碼出現了 except or finally 語句呢,此時前面說過的 PyTryBlock 就出場了。對於迴圈控制流,其 b_type 為SETUP_LOOP,

except 和 finally 分別是 SETUP_EXCEPT 和 SETUP_FINALLY,此時 b_handler 儲存著 except or finally 語句編譯後的第一條位元組

指令偏移地址。如果在發生異常後查詢到 except or finally 語句,則虛擬機器的狀態由 WHY_EXCEPTION 轉為 WHY_NOT(正常狀

態),接下去就 JUMPTO(b->b_handler)。當然如果在當前棧幀查詢到 except 語句但是異常型別不匹配,也會發起棧幀展開過程

(虛擬機器狀態變成WHY_RERAISE),即繼續向上尋找,需要注意的是 finally 語句肯定是會執行的,即使當前棧幀的 except 語句類

型不匹配。


p217: 對於一段Python 函式程式碼,對應一個PyCodeObject 物件,如果對一個函式呼叫多次,則執行時建立多個PyFunctionObject 對

象,每個物件的func_code 都指向PyCodeObject,func_globals 賦值為當前活動 Frame 的f_globals,如果有預設引數值則儲存在 func_defaults 中

(預設引數需要用不可變物件,否則執行時可能出現邏輯錯誤)。注意,使用dis.dis 檢視時,函式f 的具體實現的位元組碼指令不會出現,因為它們是

與函數f 對應的PyCodeObject 物件中。def f() 這條語句從語法上講是函式聲明語句,而從虛擬機器實現角度看是函式物件的建立語句,即宣告與定義分

離在不同PyCodeObject 物件中,類也是一樣的,類定義中的函式同理。也就是說在執行 py 時,def f()  or class A() 這樣的語句實際上會建立函式對

象 or class 物件,並新增進 module 的 f_locals 中,當然類似 import sys 這樣的操作也會新增  { 'sys' : <module 'sys' ...>} 進 f_locals。



p226: 函式引數分為位置引數 f(a, b)、鍵引數 f(a, b,name="python")、擴充套件位置引數def  f(a, b,*list)、擴充套件鍵引數 def f(a, b, **keys) 。

PyCodeObject 物件的 co_argcount 表示函式引數個數,co_nlocals 表示區域性變數個數(包含co_argcount),在def 語句中出現的引數名稱

都記錄在變數名錶co_varnames 中,它們都是函式編譯後產生的PyCodeObject 物件中的域,它們的值只能在編譯時確定,如 def  f(a, b, *list): c = 1 

則 co_argcount 為 2,co_nlocals 為4,即 *list 被當成 區域性變數,**keys 同理。如下圖函式呼叫過程中引數變化的情況,注意LOAD_FAST 與 

STORE_FAST 之間執行了 age += 5 操作。

注:在最終通過PyEval_EvalFrameEx 時,PyFunctionObject 物件的影響已經消失了,真正對新棧幀產生影響的是 PyFunctionObject 輸送的

PyCodeObject 物件和 global 名字空間,比如 PyFrame_New(tstate, co, globals, NULL) 產生新棧幀 PyFrameObject 物件,注意:傳遞給

PyFrame_New 的 global 引數是來自 PyFunctionObject.func_globals,這涉及到呼叫其他模組中定義的函式問題。Python 虛擬機器在新棧幀境中開始

一次執行新的位元組碼指令序列的迴圈,也就是函式所對應的位元組碼指令序列 PyCodeObject.co_code,新產生的Frame 的f_code 指向

此 PyCodeObject


p232 & p239 & p244: 程式碼中 extras = f->f_nlocals + ncells + nfrees ; &  freevars = f->f_localsplus + f->f_nlocals;  

f->f_nlocals 修改為 co->co_nlocals; 其實Frame 也只有f_locals 。



相關文章