閱讀源碼版本python 3.8.3
參考書籍<<Python源碼剖析>>
參考書籍<<Python學習手冊 第4版>>
官網文檔目錄介紹
- Doc目錄主要是官方文檔的說明。
- Include:目錄主要包括了Python的運行的頭文件。
- Lib:目錄主要包括了用Python實現的標準庫。
- Modules: 該目錄中包含了所有用C語言編寫的模塊,比如random、cStringIO等。Modules中的模塊是那些對速度要求非常嚴格的模塊,而有一些對速度沒有太嚴格要求的模塊,比如os,就是用Python編寫,並且放在Lib目錄下的
- Objects:該目錄中包含了所有Python的內建對象,包括整數、list、dict等。同時,該目錄還包括了Python在運行時需要的所有的內部使用對象的實現。
- Parser:該目錄中包含了Python解釋器中的Scanner和Parser部分,即對Python源碼進行詞法分析和語法分析的部分。除了這些,Parser目錄下還包含了一些有用的工具,這些工具能夠根據Python語言的語法自動生成Python語言的詞法和語法分析器,將python文件編譯生成語法樹等相關工作。
- Programs目錄主要包括了python的入口函數。
- Python:目錄主要包括了Python動態運行時執行的代碼,裡面包括編譯、字節碼解釋器等工作。
1. 總體架構
-
Runtime Env:python運行時環境,初始化對象/類型系統(Object/Type structures),內存分配器(Memory Allocator) 和 運行時狀態信息 (Current state of Python)。運行時狀態維護了解釋器在執行字節碼時不同的狀態(如正常和異常)之間的切換動作,可以視為一個巨大而複雜的有窮狀態機。內存管理機制可參考另外一篇文章Python3 源碼閱讀 – 內存管理機制。
-
Python Core: 中間部分是python的核心—-解釋器(
PyInterpreter
), 也可以成為PVM。大致流程就是 先對.py
程序進行此法分析,將文件輸入的源代碼或從命令行輸入的一行行python代碼切分一個個Token, 然後使用Parser進行語法分析,建立抽象語法樹(AST),Compiler
根據AST生成字節碼指令集合,最後由Code Evaluator
來執行這些字節碼。 -
File Groups: Python Lib庫和用戶自己的模塊包等源代碼文件
2. Run Python文件的啟動流程
Python啟動是由Programs下的python.c文件中的main函數開始執行
/* Minimal main program -- everything is loaded from the library */
#include "Python.h"
#include "pycore_pylifecycle.h"
#ifdef MS_WINDOWS
int
wmain(int argc, wchar_t **argv)
{
return Py_Main(argc, argv);
}
#else
int
main(int argc, char **argv)
{
return Py_BytesMain(argc, argv);
}
#endif
int
Py_Main(int argc, wchar_t **argv) {
...
return pymian_main(&args);
}
static int
pymain_main(_PyArgv *args)
{
PyStatus status = pymain_init(args); // 初始化
if (_PyStatus_IS_EXIT(status)) {
pymain_free();
return status.exitcode;
}
if (_PyStatus_EXCEPTION(status)) {
pymain_exit_error(status);
}
return Py_RunMain();
}
2.1 初始化關鍵流程
- 初始化一些與配置項 如:開啟utf-8模式,設置Python內存分配器
- 初始化
pyinit_core
核心部分- 創建生命周期
pycore_init_runtime
, 同時生成HashRandom - 初始化線程和解釋器並創建GIL鎖
pycore_create_interpreter
- 初始化所有基礎類型,list, int, tuple等
pycore_init_types
- 初始化sys模塊
_PySys_Create
- 初始化內建函數或者對象,如map, None, True等
pycore_init_builtins
- 其中包括內建的錯誤類型初始化
_PyBuiltins_AddExceptions
- 其中包括內建的錯誤類型初始化
- 創建生命周期
Python3.8 對Python解釋器的初始化做了重構PEP 587-Python初始化配置
2.2 run 相關源碼閱讀
int
Py_RunMain(void)
{
int exitcode = 0;
pymain_run_python(&exitcode); //執行python腳本
if (Py_FinalizeEx() < 0) { // 釋放資源
/* Value unlikely to be confused with a non-error exit status or
other special meaning */
exitcode = 120;
}
pymain_free(); // 釋放資源
if (_Py_UnhandledKeyboardInterrupt) {
exitcode = exit_sigint();
}
return exitcode;
}
static void
pymain_run_python(int *exitcode)
{
// 獲取一個持有GIL鎖的解釋器
PyInterpreterState *interp = _PyInterpreterState_GET_UNSAFE();
/* pymain_run_stdin() modify the config */
... // 添加sys_path等操作
if (config->run_command) {
// 命令行模式
*exitcode = pymain_run_command(config->run_command, &cf);
}
else if (config->run_module) {
// 模塊名
*exitcode = pymain_run_module(config->run_module, 1);
}
else if (main_importer_path != NULL) {
*exitcode = pymain_run_module(L"__main__", 0);
}
else if (config->run_filename != NULL) {
// 文件名
*exitcode = pymain_run_file(config, &cf);
}
else {
*exitcode = pymain_run_stdin(config, &cf);
}
...
}
/* Parse input from a file and execute it */ //Python/pythonrun.c
int
PyRun_AnyFileExFlags(FILE *fp, const char *filename, int closeit,
PyCompilerFlags *flags)
{
if (filename == NULL)
filename = "???";
if (Py_FdIsInteractive(fp, filename)) {
int err = PyRun_InteractiveLoopFlags(fp, filename, flags); // 是否是交互模式
if (closeit)
fclose(fp);
return err;
}
else
return PyRun_SimpleFileExFlags(fp, filename, closeit, flags); // 執行腳本
}
// 執行python .py文件
int
PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit,
PyCompilerFlags *flags)
{
...
if (maybe_pyc_file(fp, filename, ext, closeit)) {
FILE *pyc_fp;
/* Try to run a pyc file. First, re-open in binary */
...
v = run_pyc_file(pyc_fp, filename, d, d, flags);
} else {
/* When running from stdin, leave __main__.__loader__ alone */
...
v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d,
closeit, flags);
}
...
}
PyObject *
PyRun_FileExFlags(FILE *fp, const char *filename_str, int start, PyObject *globals,
PyObject *locals, int closeit, PyCompilerFlags *flags)
{
...
// // 解析傳入的腳本,解析成AST
mod = PyParser_ASTFromFileObject(fp, filename, NULL, start, 0, 0,
flags, NULL, arena);
...
// 將AST編譯成字節碼然後啟動字節碼解釋器執行編譯結果
ret = run_mod(mod, filename, globals, locals, flags, arena);
...
}
// 查看run_mode
static PyObject *
run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
PyCompilerFlags *flags, PyArena *arena)
{
...
// 將AST編譯成字節碼
co = PyAST_CompileObject(mod, filename, flags, -1, arena);
...
// 解釋執行編譯的字節碼
v = run_eval_code_obj(co, globals, locals);
Py_DECREF(co);
return v;
}
2.3 字節碼查看案例
新建test.py
def show(a):
return a
if __name__ == "__main__":
print(show(10))
執行命令: python3 -m dis test.py
λ ppython3 -m dis test.py
3 0 LOAD_CONST 0 (<code object show at 0x000000E7FC89E270, file "test.py", line 3>)
2 LOAD_CONST 1 ('show')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (show)
7 8 LOAD_NAME 1 (__name__)
10 LOAD_CONST 2 ('__main__')
12 COMPARE_OP 2 (==)
14 POP_JUMP_IF_FALSE 28
8 16 LOAD_NAME 2 (print)
18 LOAD_NAME 0 (show)
20 LOAD_CONST 3 (10)
22 CALL_FUNCTION 1
24 CALL_FUNCTION 1
26 POP_TOP
>> 28 LOAD_CONST 4 (None)
左邊3, 7, 8表示 test.py中的第一行和第二行,右邊表示python byte code
Include/opcode.h
發現總共有 163 個 opcode, 所有的 python 源文件(Lib庫中的文件)都會被編譯器翻譯成由 opcode 組成的 pyx 文件,並緩存在執行目錄,下次啟動程序如果源代碼沒有修改過,則直接加載這個pyx文件,這個文件的存在可以加快 python 的加載速度。普通.py文件如我們的test.py 是直接進行編譯解釋執行的,不會生成.pyc文件,想生成test.pyc 需要使用python內置的py_compile模塊來編譯該文件,或者執行命令python3 -m test.py
python生成.pyc文件
嚴格意義上來說: 只有文件導入import 的情況下字節碼.pyc文件才會保存下來,
__pycache__
— 《python學習手冊(第四版) Page40》
2.4 python中的code對象
字節碼在python虛擬機中對應的是PyCodeObject
對象, .pyc文件是字節碼在磁盤上的表現形式。python編譯的過程中,一個代碼塊就對應一個code對象,那麼如何確定多少代碼算是一個Code Block呢? 編譯過程中遇到一個新的命名空間或者作用域時就生成一個code對象,即類或函數都是一個代碼塊,一個code的類型結構就是PyCodeObject
, 參考Junnplus
/* Bytecode object */
typedef struct {
PyObject_HEAD
int co_argcount; /* #arguments, except *args */ // 位置參數的個數,
int co_posonlyargcount; /* #positional only arguments */
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 */
int co_firstlineno; /* first source line number */
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. 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.
*/
Py_ssize_t *co_cell2arg; /* Maps cell vars which are arguments. */
PyObject *co_filename; /* unicode (where it was loaded from) */
PyObject *co_name; /* unicode (name, for reference) */
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 */
/* Scratch space for extra data relating to the code object.
Type is a void* to keep the format private in codeobject.c to force
people to go through the proper APIs. */
void *co_extra;
/* Per opcodes just-in-time cache
*
* To reduce cache size, we use indirect mapping from opcode index to
* cache object:
* cache = co_opcache[co_opcache_map[next_instr - first_instr] - 1]
*/
// co_opcache_map is indexed by (next_instr - first_instr).
// * 0 means there is no cache for this opcode.
// * n > 0 means there is cache in co_opcache[n-1].
unsigned char *co_opcache_map;
_PyOpcache *co_opcache;
int co_opcache_flag; // used to determine when create a cache.
unsigned char co_opcache_size; // length of co_opcache.
} PyCodeObject;
Field | Content | Type |
---|---|---|
co_argcount | Code Block 的參數個數 | PyIntObject |
co_posonlyargcount | Code Block 的位置參數個數 | PyIntObject |
co_kwonlyargcount | Code Block 的關鍵字參數個數 | PyIntObject |
co_nlocals | Code Block 中局部變量的個數 | PyIntObject |
co_stacksize | Code Block 的棧大小 | PyIntObject |
co_flags | N/A | PyIntObject |
co_firstlineno | Code Block 對應的 .py 文件中的起始行號 | PyIntObject |
co_code | Code Block 編譯所得的字節碼 | PyBytesObject |
co_consts | Code Block 中的常量集合 | PyTupleObject |
co_names | Code Block 中的符號集合 | PyTupleObject |
co_varnames | Code Block 中的局部變量名集合 | PyTupleObject |
co_freevars | Code Block 中的自由變量名集合 | PyTupleObject |
co_cellvars | Code Block 中嵌套函數所引用的局部變量名集合 | PyTupleObject |
co_cell2arg | N/A | PyTupleObject |
co_filename | Code Block 對應的 .py 文件名 | PyUnicodeObject |
co_name | Code Block 的名字,通常是函數名/類名/模塊名 | PyUnicodeObject |
co_lnotab | Code Block 的字節碼指令於 .py 文件中 source code 行號對應關係 | PyBytesObject |
co_opcache_map | python3.8新增字段,存儲字節碼索引與CodeBlock對象的映射關係 | PyDictObject |
2.4.1 LOAD_CONST
// Python\ceval.c
PREDICTED(LOAD_CONST); -> line 943: #define PREDICTED(op) PRED_##op:
FAST_DISPATCH(); -> line 876 #define FAST_DISPATCH() goto fast_next_opcode
額外收穫: c 語言中 ##和# 號 在marco 里的作用可以參考 這篇
在宏定義里, ## 被稱為連接符(concatenator) , a##b 表示將ab連接起來
a 表示把a轉換成字符串,即加雙引號,
所以LONAD_CONST這個指領根據宏定義展開如下:
case TARGET(LOAD_CONST): {
PRED_LOAD_CONST:
PyObject *value = GETITEM(consts, oparg); // 獲取一個PyObject* 指針對象
Py_INCREF(value); // 引用計數加1
PUSH(value); // 把剛剛創建的PyObject* push到當前的frame的stack上, 以便下一個指令從這個 stack 上面獲取
goto fast_next_opcode;
2.5 main_loop
// Python\ceval.c
main_loop:
for (;;) {
...
switch (opcode) {
/* BEWARE!
It is essential that any operation that fails must goto error
and that all operation that succeed call [FAST_]DISPATCH() ! */
case TARGET(NOP): {
FAST_DISPATCH();
}
case TARGET(LOAD_FAST): {
PyObject *value = GETLOCAL(oparg);
if (value == NULL) {
format_exc_check_arg(PyExc_UnboundLocalError,
UNBOUNDLOCAL_ERROR_MSG,
PyTuple_GetItem(co->co_varnames, oparg));
goto error;
}
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}
case TARGET(LOAD_CONST): {
PREDICTED(LOAD_CONST);
PyObject *value = GETITEM(consts, oparg);
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}
...
}
}
在 python 虛擬機中,解釋器主要在一個很大的循環中,不停地讀入 opcode, 並根據 opcode 執行對應的指令,當執行完所有指令虛擬機退出,程序也就結束了
2.6 總結
過程描述:
- python先把代碼(.py文件)編譯成字節碼,交給字節碼虛擬機,然後虛擬機會從編譯得到的
PyCodeObject
對象中一條一條執行字節碼指令,並在當前的上下文環境中執行這條字節碼指令,從而完成程序的執行。Python虛擬機實際上是在模擬操作中執行文件的過程。PyCodeObject
對象中包含了字節碼指令以及程序的所有靜態信息,但沒有包含程序運行時的動態信息——執行環境(PyFrameObject
),後面會繼續記錄執行環境的閱讀。 - 從整體上看:OS中執行程序離不開兩個概念:進程和線程。python中模擬了這兩個概念,模擬進程和線程的分別是PyInterpreterState和PyTreadState。即:每個
PyThreadState
都對應着一個幀棧,python虛擬機在多個線程上切換(靠GIL實現線程之間的同步)。當python虛擬機開始執行時,它會先進行一些初始化操作,最後進入PyEval_EvalFramEx函數,內部實現了一個main_loop
它的作用是不斷讀取編譯好的字節碼,並一條一條執行,類似CPU執行指令的過程。函數內部主要是一個switch
結構,根據字節碼的不同執行不同的代碼
3. Python中的Frame
如上所說,PyCodeObject
對象只是包含了字節碼指令集以及程序的相關靜態信息,虛擬機的執行還需要一個執行環境,即PyFrameObject
,也就是對系統棧幀的模擬。
3.1 堆和棧的認識
堆中存的是對象。棧中存的是基本數據類型和堆中對象的引用。一個對象的大小是不可估計的,或者說是可以動態變化的,但是在棧中,一個對象只對應了一個4btye的引用(堆棧分離的好處)
內存中的堆棧和數據結構堆棧不是一個概念,可以說內存中的堆棧是真實存在的物理區,數據結構中的堆棧是抽象的數據存儲結構。
內存空間在邏輯上分為三部分:代碼區,靜態數據區和動態數據區,動態數據區有分為堆區和棧區
- 代碼區:存儲的二進制代碼塊,高級調度(作業調度)、中級調度(內存調度)、低級調度(進程調度)控制代碼區執行代碼的切換
- 靜態數據區:存儲全局變量,靜態變量,常量,系統自動分配和回收。
- 動態數據區:
- 棧區(stack):存儲運行方法的形參,局部變量,返回值,有編譯器自動分配和回收,操作類似數據結構中的棧
- 堆區(heap):new一個對象的引用或者地址存儲在棧區,該地址指向指向對象存儲在堆區中的真實數據。如c中的
malloc
函數,python中的Pymalloc
3.2 PyFrameObject對象
typedef struct _frame{
PyObject_VAR_HEAD //"運行時棧"的大小是不確定的, 所以用可變長的對象
struct _frame *f_back; //執行環境鏈上的前一個frame,很多個PyFrameObject連接起來形成執行環境鏈表
PyCodeObject *f_code; //PyCodeObject 對象,這個frame就是這個PyCodeObject對象的上下文環境
PyObject *f_builtins; //builtin名字空間
PyObject *f_globals; //global名字空間
PyObject *f_locals; //local名字空間
PyObject **f_valuestack; //"運行時棧"的棧底位置
PyObject **f_stacktop; //"運行時棧"的棧頂位置
//...
int f_lasti; //上一條字節碼指令在f_code中的偏移位置
int f_lineno; //當前字節碼對應的源代碼行
//...
//動態內存,維護(局部變量+cell對象集合+free對象集合+運行時棧)所需要的空間
PyObject *f_localsplus[1];
} PyFrameObject;
如果你想知道 PyFrameObject 中每個字段的意義, 請參考 Junnplus’ blog 或者直接閱讀源代碼,了解frame的執行過程可以參考zpoint’blog.
名字空間實際上是維護着變量名和變量值之間關係的PyDictObject對象。
f_builtins, f_globals, f_locals名字空間分別維護了builtin, global, local的name與對應值之間的映射關係。
每一個 PyFrameObject對象都維護了一個 PyCodeObject對象,這表明每一個 PyFrameObject中的動態內存空間對象都和源代碼中的一段Code相對應。
3.2.1 棧幀的獲取,工作中會用到
可以通過sys._getframe([depth]), 獲取指定深度的PyFrameObject
對象
>>> import sys
>>> frame = sys._getframe()
>>> frame
<frame object at 0x103ab2d48>
3.2.2 python中變量名的解析規則 LEGB
Local -> Enclosed -> Global -> Built-In
-
Local 表示局部變量
-
Enclosed 表示嵌套的變量
-
Global 表示全局變量
-
Built-In 表示內建變量
如果這幾個順序都取不到,就會拋出 ValueError
可以在這個網站python執行可視化網站,觀察代碼執行流程,以及變量的轉換賦值情況。
4. 額外收穫
意外收穫: 之前知道pythonGIL , 遇到I/O阻塞時會釋放gil,現在從源碼中看到了對應的流程
if (_Py_atomic_load_relaxed(&ceval->gil_drop_request)) {
/* Give another thread a chance */
if (_PyThreadState_Swap(&runtime->gilstate, NULL) != tstate) {
Py_FatalError("ceval: tstate mix-up");
}
drop_gil(ceval, tstate);
/* Other threads may run now */
take_gil(ceval, tstate);
/* Check if we should make a quick exit. */
exit_thread_if_finalizing(runtime, tstate);
if (_PyThreadState_Swap(&runtime->gilstate, tstate) != NULL) {
Py_FatalError("ceval: orphan tstate");
}
}
/* Check for asynchronous exceptions. */
參考:
python 源碼分析 基本篇
python虛擬機運行原理
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※超省錢租車方案
※別再煩惱如何寫文案,掌握八大原則!
※回頭車貨運收費標準
※教你寫出一流的銷售文案?
※產品缺大量曝光嗎?你需要的是一流包裝設計!
※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益
※網頁設計最專業,超強功能平台可客製化