本系列是以陳儒先生的《python源碼剖析》為學習素材,所記錄的學習內容。不同的是陳儒先生的《python源碼剖析》所剖析的是python2.5,本系列對應的是python3.7,所以某些地方會和原著有出入,另外我在介紹的過程中會穿插大量的python代碼,不僅僅是介紹如何實現的,還會使用python實際地對我們的結論進行演示。下面就開始吧。不過在開始分析python的實現之前,我們有很多的准備工作要做。比如,首先應該了解一下python的整體架構,來對python的實現有一個宏觀的認識
0.1 python的總體架構
廢話不多說,先來看一張python的總體架構圖。
如圖所示。在最高的層次上,python的總體架構可以分為三個主要的部分。圖的左邊,是python所提供的的大量的模塊、庫、以及用戶自定義的模塊。比如在執行import os
的時候,這個os就是python內建的模塊,當然用戶還可以通過自定義模塊來擴展python系統。
在圖的的右邊,是python的運行時環境,包括對象/類型系統(Object/Type Structures)、內存分配器(Memory Allocator)、和運行時狀態信息(Current State of Python)。運行時狀態
維護了解釋器在執行字節碼時不同的狀態(比如正常狀態和異常狀態)之間切換的動作,我們可以將它視為一個巨大而復雜的有窮狀態機。內存分配器則全權負責python中創建對象時,對內存的申請工作,實際上它就是python運行時與C中malloc的一層接口。而對象/類型系統則包含了python中存在的各種內建對象,比如:整數、list、dict,以及各種用戶自定義的類型和對象。
在中間的部分,可以看到python的核心--解釋器(interpreter),或者稱之為虛擬機。在解析器中,箭頭的方向指示了python運行過程中的數據流方向。其中scanner對應詞法分析,將文件輸入的python源代碼或者從命令行輸入的一行行python代碼切分為一個個的token;parser對應語法分析,在scanner的分析結果上進行語法分析,建立抽象語法樹(Abstract Syntax Tree,簡稱AST );Compiler則是對AST進行編譯,生成指令集合--也就是python字節碼(byte code);最后由Code Evaluator來執行這些字節碼。因此Code Evaluator又可以被稱為虛擬機。
Code Evaluator和Object/Type Structure之間的箭頭表示使用關系;而與Current State of Python之間的箭頭表示修改關系,即python在執行的過程中會不斷地修改當前解釋器所處的狀態,在不同的狀態之間切換。
0.2 python源代碼的組織
python的源代碼可以在python的官網www.python.org
中下載,下載完源碼的壓縮包並解壓之后,可以看到如下的目錄結構。
Include
:該目錄包含了python所提供的的所有頭文件,如果用戶需要自己使用C或者C++來編寫自定義模塊擴展python,那么就需要用到這里的頭文件Lib
:該目錄包含了python自帶的所有標准庫,Lib中的庫基本上都是使用python編寫的Modules
:該目錄中包含了所有用C語言編寫的模塊,比如random、io等。Modules中的模塊是那些對速度要求非常嚴格的模塊,而有一些對速度沒有太嚴格要求的模塊,比如os,就是用python編寫,並且是放在Lib目錄下的。Parser
:該目錄中包含了python解釋器中的Scanner和Parser部分,即對python源代碼進行詞法分析和語法分析的部分。除了這些,Parser還包含了一些有用的工具,這些工具能夠根據python語言的語法自動生成python語言的詞法和語法分析器,與YACC非常類似Objects
:該目錄包含了所有python的內建對象,包括整數、list、tuple、dict、set等等。同時,該目錄還包含了python在運行時需要的所有內部使用對象的實現。Python
:該目錄包含了python解釋器中Compiler(編譯)和Code Evaluator(執行)兩部分,是python運行的核心所在
0.3 修改python源代碼
怎么上來就修改python源代碼,別慌,只是簡單的做一個小小的trick。
//文件:Objects/listobject.c。這是python中與list實現有關的源文件。
//可以看到這個函數主要用來改變列表的容量的。
static int
//關於這里的Py_ssize_t,這是python自定義的類型,就把它當成int即可
list_resize(PyListObject *self, Py_ssize_t newsize)
{
PyObject **items;
size_t new_allocated, num_allocated_bytes;
Py_ssize_t allocated = self->allocated;
//這里的allocated是當前list對象所擁有的容量,注意不是list對象的長度(或者說元素個數),而是容量
//比如 l = [1, 2, 3, 4, 5, 6, 7, 8],如果是使用[]這種中括號的方式創建列表的話。那么顯然這里的l的長度和容量都為8。
//而這里的newsize是,當我們append或者extend新元素之后,對應的list對象的長度
//當使用l.append(9)的時候,說明了什么,說明了newsize變成了9
//可是原來的allocated(容量)是8啊,所以要進行擴容了。
//看這里的if條件,顯然allocated >= newsize已經不滿足了,因為要存儲的元素的個數超過了容量,要擴容了
if (allocated >= newsize && newsize >= (allocated >> 1)) {
assert(self->ob_item != NULL || newsize == 0);
Py_SIZE(self) = newsize;
return 0;
}
//擴容之后的容量為new_allocated = newsize + newsize >> 3 + newsize < 9 ? 3 : 6
//所以將9帶入進入,得到 9 + 1 + 6 = 16,這便是新分配的容量
/*
l = [1, 2, 3, 4, 5, 6, 7, 8]
print((l.__sizeof__() - 40) // 8) # 8
l.append(9)
print((l.__sizeof__() - 40) // 8) # 16
*/
//使用python進行測試也驗證了這一點
new_allocated = (size_t)newsize + (newsize >> 3) + (newsize < 9 ? 3 : 6);
if (new_allocated > (size_t)PY_SSIZE_T_MAX / sizeof(PyObject *)) {
PyErr_NoMemory();
return -1;
}
if (newsize == 0)
new_allocated = 0;
num_allocated_bytes = new_allocated * sizeof(PyObject *);
//將新添加的元素的指針加進列表里面
items = (PyObject **)PyMem_Realloc(self->ob_item, num_allocated_bytes);
if (items == NULL) {
PyErr_NoMemory();
return -1;
}
//讓ob_item指向為新的items
self->ob_item = items;
Py_SIZE(self) = newsize;
//將這里的allocated變為新分配的容量
self->allocated = new_allocated;
return 0;
}
// 中間省略了一部分
//可以看到這是通過索引獲取list對象內部元素的實現
static PyObject *
list_item(PyListObject *a, Py_ssize_t i)
{
//這里的i就是我們傳入的索引。
//如果i小於0或者i大於op(對應的list對象)的最大索引
if (i < 0 || i >= Py_SIZE(a)) {
//當然python中list對象也支持負數索引,因此還會有其他的檢測
if (indexerr == NULL) {
//PyUnicode_FromString可以理解為在python代碼報錯的時候,存儲打印信息的函數
indexerr = PyUnicode_FromString(
//看這行代碼熟悉不,小伙伴們,索引越界是不是報這個錯誤啊
"list index out of range");//我們將原本的信息改一下,改成"兄嘚,您列表索引越界了"
if (indexerr == NULL)
return NULL;
}
//設置異常,這是一個IndexError,indexerr則是異常信息
PyErr_SetObject(PyExc_IndexError, indexerr);
return NULL;
}
Py_INCREF(a->ob_item[i]);
//如果i為是正常索引,直接返回。
return a->ob_item[i];
}
0.4 編譯python
編譯python:只介紹如何在linux下編譯python。編譯的過程分為三步
進入python的主目錄下,執行:./configure -prefix=你期望python安裝到的路徑
make
make install
其中2、3兩步可以合為一步,即make && make install
,下面就用我們剛才修改完的python源碼進行編譯
可以看到異常,已經被我們從底層重新定義了。
0.5 注意事項
- 在早期版本,python的許多數值的類型都是int或者long,現在Python自己定義了一個新類型,Py_ssize_t,凡出現這個類型的地方,就把它當成long看待即可
- python的內存管理機制是比較復雜的,我們會在后面剖析。但是很快你就會看到對內存管理接口的使用。這是因為創建對象必先分配內存,而分配內存必須通過內存管理接口,所以我們在這里提一下。通常Python的源碼中會使用PyObject_GC_NEW、PyObject_GC_Malloc、PyMem_MALLOC,PyObject_MALLOC等API。只要堅持一個原則即可,凡是以New結尾的,都當成是C++中new操作符即可,凡是以Malloc結尾的,都當成是C中的malloc操作符即可。