第二部分:Python解釋器進程
在上節教你閱讀 Cpython 的源碼(一)中,我們從編寫Python到執行代碼的過程中看到Python語法和其內存管理機制。
在本節,我們將從代碼層面去討論 ,Python的編譯過程。
調用Python二進制文件可以通過以下五種方式:
1.使用-c和Python命令運行單個命令
2.使用-m和模塊名稱啟動模塊
3.使用文件名運行文件
4.使用shell管道運行stdin輸入
5.啟動REPL並一次執行一個命令
整個運行過程你可以通過檢查下面三個源文件進行了解:
1.Programs/python.c
是一個簡單的入口文件。
2.Modules/main.c
匯集加載配置,執行代碼和清理內存整個過程的代碼文件。
3.Python/initconfig.c
從系統環境加載配置,並將其與任何命令行標志合並。
此圖顯示了如何調用每個函數:
執行模式由配置確定。
CPython源代碼樣式:
與Python代碼的PEP8樣式指南類似,CPython C代碼有一個官方樣式指南,最初於2001年設計並針對現代版本進行了更新。
這里有一些命名標准方便你調試跟蹤源代碼:
- 對公共函數使用Py前綴,靜態函數不使用。Py_前綴保留用於
Py_FatalError
等全局服務例程。特定的對象(如特定的對象類型API)使用較長的前綴,例如PyString_
用於字符串函數。 - 公眾函數和變量,使用首寫字母大寫,單詞之間下划線分割的形式,
例如:PyObject_GetAttr
,Py_BuildValue
,PyExc_TypeError
。 - 有時,加載器必須能夠看到內置函數。
我們使用_Py前綴,例如_PyObject_Dump
。 - 宏應具有混合字母前綴,首字母大寫,例如
PyString_AS_STRING
,Py_PRINT_RAW
。
創建運行環境的配置
通過上圖可以看到,在執行Python代碼之前,首先會建立配置。在文件Include/cpython/initconfig.h
中名為PyConfig
的對象會定義一個配置的數據結構。
配置數據結構包括以下內容:
- 各種模式的運行時標志,如調試和優化模式
- 執行模式,例如是否傳遞了文件名,提供了stdin或模塊名稱
- 擴展選項,由-X
- 運行時設置的環境變量
配置數據主要是CPython在運行時用於啟用和禁用各種功能。Python還附帶了幾個命令行界面選項。
在Python中,你可以使用-v標志啟用詳細模式。在詳細模式下,Python將在加載模塊時將消息打印到屏幕:
$ ./python.exe -v -c "print('hello world')"
# installing zipimport hook
import zipimport # builtin
# installed zipimport hook
...
你可以在PyConfig
的struct中的Include/cpython/initconfig.h
中看到此標志的定義:
/* --- PyConfig ---------------------------------------------- */
typedef struct {
int _config_version; /* Internal configuration version,
used for ABI compatibility */
int _config_init; /* _PyConfigInitEnum value */
...
/* If greater than 0, enable the verbose mode: print a message each time a
module is initialized, showing the place (filename or built-in module)
from which it is loaded.
If greater or equal to 2, print a message for each file that is checked
for when searching for a module. Also provides information on module
cleanup at exit.
Incremented by the -v option. Set by the PYTHONVERBOSE environment
variable. If set to -1 (default), inherit Py_VerboseFlag value. */
int verbose;
在Python/initconfig.c
中,建立了從環境變量和運行時命令行標志讀取設置的邏輯。
在config_read_env_vars
函數中,讀取環境變量並用於為配置設置分配值:
static PyStatus
config_read_env_vars(PyConfig *config)
{
PyStatus status;
int use_env = config->use_environment;
/* 獲取環境變量 */
_Py_get_env_flag(use_env, &config->parser_debug, "PYTHONDEBUG");
_Py_get_env_flag(use_env, &config->verbose, "PYTHONVERBOSE");
_Py_get_env_flag(use_env, &config->optimization_level, "PYTHONOPTIMIZE");
_Py_get_env_flag(use_env, &config->inspect, "PYTHONINSPECT");
對於詳細設置,你可以看到如果PYTHONVERBOSE
存在,PYTHONVERBOSE
的值用於設置&config-> verbose
的值,如果環境變量不存在,則將保留默認值-1。
然后再次在initconfig.c
中的config_parse_cmdline
函數中,用命令行標志來設置值:
static PyStatus
config_parse_cmdline(PyConfig *config, PyWideStringList *warnoptions,
Py_ssize_t *opt_index)
{
...
switch (c) {
...
case 'v':
config->verbose++;
break;
...
/* This space reserved for other options */
default:
/* unknown argument: parsing failed */
config_usage(1, program);
return _PyStatus_EXIT(2);
}
} while (1);
此值之后由_Py_GetGlobalVariablesAsDict
函數復制到全局變量Py_VerboseFlag
。
在Python中,可以使用具名元組類型的對象sys.flags
訪問運行時標志,如詳細模式,安靜模式。-X標志在sys._xoptions
字典中都可用。
$ ./python.exe -X dev -q
>>> import sys
>>> sys.flags
sys.flags(debug=0, inspect=0, interactive=0, optimize=0, dont_write_bytecode=0,
no_user_site=0, no_site=0, ignore_environment=0, verbose=0, bytes_warning=0,
quiet=1, hash_randomization=1, isolated=0, dev_mode=True, utf8_mode=0)
>>> sys._xoptions
{'dev': True}
除了initconfig.h
中的運行時配置外,還有構建配置,它位於根文件夾中的pyconfig.h
內。
此文件在構建過程的配置步驟中動態創建,或由Visual Studio for Windows系統動態創建。
可以通過運行以下命令查看構建配置:
$ ./python.exe -m sysconfig
讀取文件/輸入
一旦CPython具有運行時配置和命令行參數,就可以確定它需要執行的內容了。
此任務由Modules/main.c
中的pymain_main
函數處理。
根據新創建的配置實例,CPython現在將執行通過多個選項提供的代碼。
通過-c輸入
最簡單的是為CPython提供一個帶-c選項的命令和一個帶引號的Python代碼。
例如:
$ ./python.exe -c "print('hi')"
hi
下圖是整個過程的流程圖
首先,在modules/main.c
中執行pymain_run_command
函數,將在-c中傳遞的命令作為C程序中wchar_t *
的參數。
wchar_t*
類型通常被用作Cpython中Unicode的低級存儲數據類型,因為該類型的大小可以存儲utf8字符。
將wchar_t *
轉換為Python字符串時,Objects/unicodetype.c
文件有一個輔助函數PyUnicode_FromWideChar
,它會返回一個PyObject
,其類型為str。然后,通過PyUnicode_AsUTF8String
,完成對UTF8的編碼,並將Python中的str對象轉換為Python字節類型。
完成后,pymain_run_command
會將Python字節對象傳遞給PyRun_SimpleStringFlags
執行,但首先會通過 PyBytes_AsString
將字節對象再次轉換為str類型。
static int
pymain_run_command(wchar_t *command, PyCompilerFlags *cf)
{
PyObject *unicode, *bytes;
int ret;
unicode = PyUnicode_FromWideChar(command, -1);
if (unicode == NULL) {
goto error;
}
if (PySys_Audit("cpython.run_command", "O", unicode) < 0) {
return pymain_exit_err_print();
}
bytes = PyUnicode_AsUTF8String(unicode);
Py_DECREF(unicode);
if (bytes == NULL) {
goto error;
}
ret = PyRun_SimpleStringFlags(PyBytes_AsString(bytes), cf);
Py_DECREF(bytes);
return (ret != 0);
error:
PySys_WriteStderr("Unable to decode the command from the command line:\n");
return pymain_exit_err_print();
}
將wchar_t *
轉換為Unicode,字節,然后轉換為字符串大致相當於以下內容:
unicode = str(command)
bytes_ = bytes(unicode.encode('utf8'))
# call PyRun_SimpleStringFlags with bytes_
PyRun_SimpleStringFlags
函數是Python/pythonrun.c
的一部分。它的目的是將這個簡單的命令轉換為Python模塊,然后將其發送以執行。由於Python
模塊需要將__main__
作為獨立模塊執行,因此它會自動創建。
int
PyRun_SimpleStringFlags(const char *command, PyCompilerFlags *flags)
{
PyObject *m, *d, *v;
m = PyImport_AddModule("__main__"); #創建__main__模塊
if (m == NULL)
return -1;
d = PyModule_GetDict(m);
v = PyRun_StringFlags(command, Py_file_input, d, d, flags);
if (v == NULL) {
PyErr_Print();
return -1;
}
Py_DECREF(v);
return 0;
}
一旦PyRun_SimpleStringFlags
創建了一個模塊和一個字典,它就會調用PyRun_StringFlags
函數,它會創建一個偽文件名,然后調用Python解析器從字符串創建一個AST並返回一個模塊,mod
。
你將在下一節中深入研究AST和Parser代碼。
通過-m輸入
執行 Python 命令的另一個方法,通過使用 -m 然后知道一個模塊名。一個典型的例子是python -m unittest
,運行一個unittest
測試模塊。使用-m標志意味着在模塊包中,你想要執行__main__
中的任何內容。它還意味着你要在sys.path
中搜索指定的模塊。所以,使用這種搜索機制之后,你不需要去記憶unittest
模塊它位於那個位置。
為什么會這樣呢?接下來就讓我們一起看看原因。
在Modules/main.c
中,當使用-m標志運行命令行時,它會調用pymain_run_module
函數,並將傳入模塊的名稱作為modname
參數傳遞。
然后CPython將導入標准庫模塊runpy
,並通過PyObject_Call
函數執行它。導入模塊的操作是在函數PyImport_ImportModule
進行的。
static int
pymain_run_module(const wchar_t *modname, int set_argv0)
{
PyObject *module, *runpy, *runmodule, *runargs, *result;
runpy = PyImport_ImportModule("runpy");
...
runmodule = PyObject_GetAttrString(runpy, "_run_module_as_main");
...
module = PyUnicode_FromWideChar(modname, wcslen(modname));
...
runargs = Py_BuildValue("(Oi)", module, set_argv0);
...
result = PyObject_Call(runmodule, runargs, NULL);
...
if (result == NULL) {
return pymain_exit_err_print();
}
Py_DECREF(result);
return 0;
}
在這個函數中,您還將看到另外兩個C API函數:PyObject_Call
和PyObject_GetAttrString
。
因為PyImport_ImportModule
返回一個核心對象類型PyObject *
,所以需要調用特殊函數來獲取屬性並調用它。
在Python中,如果你需要調用某個函數屬性,你可以使用getattr()
函數。類似的,在C API中,它將調用Objects/object.c
文件中的 PyObject_GetAttrString
方法。如果你要在python中運行一個callable
類型的對象,你需要使用括號運行它,或者調用其__call__()
屬性。在Objects/object.c
中對__call__()
進行了實現。
hi = "hi!"
hi.upper() == hi.upper.__call__() # this is the same
runpy
模塊就在Lib/runpy.py
,它是純Python寫的。
執行python -m <module>
相當於運行python -m runpy <module>
。
創建runpy模塊是為了抽象在操作系統上定位和執行模塊的過程。
runpy做了一些事情來運行目標模塊:
- 為你提供的模塊名稱調用
\__import __()
- 將
\__name__
(模塊名稱)設置為名為\__main__
的命名空間 - 在
\__main__
命名空間內執行該模塊
runpy模塊還支持執行目錄和zip文件。
通過文件名輸入
如果Python命令的第一個參數是文件名,例如,python test.py
。Cpython會打開一個文件的句柄,類似我們在Python中使用open(),並將句柄傳遞給Python/pythonrun.c.
文件里的PyRun_SimpleFileExFlags()
。
這里有三種方式:
1.如果文件后綴是.pyc,就會調用run_pyc_file()
。
2.如果文件后綴是.py,將調用PyRun_FileExFlags()
。
3.如果文件路徑是stdin,用戶運行了命令| python會將stdin視為文件句柄並運行PyRun_FileExFlags()
。
下面是上述過程的C代碼
int
PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit,
PyCompilerFlags *flags)
{
...
m = PyImport_AddModule("__main__");
...
if (maybe_pyc_file(fp, filename, ext, closeit)) {
...
v = run_pyc_file(pyc_fp, filename, d, d, flags);
} else {
/* When running from stdin, leave __main__.__loader__ alone */
if (strcmp(filename, "<stdin>") != 0 &&
set_main_loader(d, filename, "SourceFileLoader") < 0) {
fprintf(stderr, "python: failed to set __main__.__loader__\n");
ret = -1;
goto done;
}
v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d,
closeit, flags);
}
...
return ret;
}
使用PyRun_FileExFlags()通過文件輸入
對於使用stdin和腳本文件方式,CPython會將文件句柄傳遞給位於pythonrun.c
文件中的PyRun_FileExFlags()
。PyRun_FileExFlags()的目的類似於用於-c
輸入PyRun_SimpleStringFlags()
,Cpython會把文件句柄加載到PyParser_ASTFromFileObject()
中。
我們將在下一節介紹Parser和AST模塊
因為這是一個完整的腳本,所以它不用像使用-c的方式需要通過PyImport_AddModule("__main__")
創建__main__模塊。
與PyRun_SimpleStringFlags
相同,一旦PyRun_FileExFlags()
從文件創建了一個Python模塊,它就會將它發送到run_mod()
來執行。
run_mod()可以在Python/pythonrun.c
中找到,並將模塊發送到AST以編譯成代碼對象,代碼對象是用於存儲字節碼操作的格式,並保存到.pyc文件中。
C代碼片段
static PyObject *
run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
PyCompilerFlags *flags, PyArena *arena)
{
PyCodeObject *co;
PyObject *v;
co = PyAST_CompileObject(mod, filename, flags, -1, arena);
if (co == NULL)
return NULL;
if (PySys_Audit("exec", "O", co) < 0) {
Py_DECREF(co);
return NULL;
}
v = run_eval_code_obj(co, globals, locals);
Py_DECREF(co);
return v;
}
我們將在下一節中介紹CPython編譯器和字節碼。
對run_eval_code_obj()
的調用是一個簡單的包裝函數,然后它會調用Python/eval.c
文件中的PyEval_EvalCode()
函數。PyEval_EvalCode()函數是CPython的主要評估循環,它會迭代每個字節碼語句並在本地機器上執行它。
使用run_pyc_file() 通過編譯字節碼輸入
在PyRun_SimpleFileExFlags()
中,有一個判斷子句為用戶提供了.pyc文件的文件路徑。如果文件路徑以.pyc結尾,則不是將文件作為純文本文件加載並解析它,它會假定.pyc文件的內容是字節碼,並保存到磁盤中。
文件Python/pythonrun.c
中的run_py_file()
方法,使用文件句柄從.pyc文件中編組(marshals)代碼對象。編組(Marshaling)是一個技術術語,作用是將文件內容復制到內存中並將其轉換為特定的數據結構。磁盤上的代碼對象數據結構是CPython編譯器緩存已編譯代碼的方式,因此每次調用腳本時都不需要解析它。
C代碼
static PyObject *
run_pyc_file(FILE *fp, const char *filename, PyObject *globals,
PyObject *locals, PyCompilerFlags *flags)
{
PyCodeObject *co;
PyObject *v;
...
v = PyMarshal_ReadLastObjectFromFile(fp);
...
if (v == NULL || !PyCode_Check(v)) {
Py_XDECREF(v);
PyErr_SetString(PyExc_RuntimeError,
"Bad code object in .pyc file");
goto error;
}
fclose(fp);
co = (PyCodeObject *)v;
v = run_eval_code_obj(co, globals, locals);
if (v && flags)
flags->cf_flags |= (co->co_flags & PyCF_MASK);
Py_DECREF(co);
return v;
}
一旦代碼對象被封送到內存,它就被發送到run_eval_code_obj()
,它會調用Python/ceval.c
來執行代碼。
詞法分析(Lexing)和句法分析(Parsing)
在閱讀和執行 Python 文件的過程中,我們深入了解了解析器和AST模塊,並對函數PyParser_ASTFromFileObject()
函數進行了調用。
我們繼續看Python/pythonrun.c
,該文件的PyParser_ASTFromFileObject()
方法將拿到一個文件句柄,編譯器標志和PyArena實例,並使用PyParser_ParseFileObject()
將文件對象轉換為節點對象。
節點對象將使用AST函數PyAST_FromNodeObject
轉換為模塊。
C代碼
mod_ty
PyParser_ASTFromFileObject(FILE *fp, PyObject *filename, const char* enc,
int start, const char *ps1,
const char *ps2, PyCompilerFlags *flags, int *errcode,
PyArena *arena)
{
...
node *n = PyParser_ParseFileObject(fp, filename, enc,
&_PyParser_Grammar,
start, ps1, ps2, &err, &iflags);
...
if (n) {
flags->cf_flags |= iflags & PyCF_MASK;
mod = PyAST_FromNodeObject(n, flags, filename, arena);
PyNode_Free(n);
...
return mod;
}
談到了PyParser_ParseFileObject()
函數,我們需要切換到Parser/parsetok.c
文件以及談談CPython解釋器的解析器-標記化器階段。
此函數有兩個重要任務:
1.在Parser/tokenizer.c
中使用PyTokenizer_FromFile()
實例化標記化器狀態tok_state結構體。
2.使用Parser/parsetok.c
中的parsetok()
將標記(tokens)轉換為具體的解析樹(節點列表)。
node *
PyParser_ParseFileObject(FILE *fp, PyObject *filename,
const char *enc, grammar *g, int start,
const char *ps1, const char *ps2,
perrdetail *err_ret, int *flags)
{
struct tok_state *tok;
...
if ((tok = PyTokenizer_FromFile(fp, enc, ps1, ps2)) == NULL) {
err_ret->error = E_NOMEM;
return NULL;
}
...
return parsetok(tok, g, start, err_ret, flags);
}
tok_state(在Parser/tokenizer.h中定義)是存儲由tokenizer生成的所有臨時數據的數據結構。它被返回到解析器-標記器(parser-tokenizer),因為parsetok()
需要數據結構來開發具體的語法樹。
在parsetok()
的內部,他會調用結構體tok_state,在循環中調用tok_get(),直到文件耗盡並且找不到更多的標記(tokens)為止。
tok_get()
位於Parser/tokenizer.c
文件,其實為類型迭代器(iterator),它將繼續返回解析樹中的下一個token。
tok_get()是整個CPython代碼庫中最復雜的函數之一。它有超過640行的代碼,包括數十年的邊緣案例,以及新語言功能和語法。
其中一個比較簡單的例子是將換行符轉換為NEWLINE標記的部分:
static int
tok_get(struct tok_state *tok, char **p_start, char **p_end)
{
...
/* Newline */
if (c == '\n') {
tok->atbol = 1;
if (blankline || tok->level > 0) {
goto nextline;
}
*p_start = tok->start;
*p_end = tok->cur - 1; /* Leave '\n' out of the string */
tok->cont_line = 0;
if (tok->async_def) {
/* We're somewhere inside an 'async def' function, and
we've encountered a NEWLINE after its signature. */
tok->async_def_nl = 1;
}
return NEWLINE;
}
...
}
在這個例子里,NEWLINE是一個標記(tokens),其值在Include/token.h
中定義。
所有標記都是常量int值,並且在我們運行make regen-grammar
時生成了Include/token.h
文件。
PyParser_ParseFileObject()
返回的node類型對下一階段至關重要,它會將解析樹轉換為抽象語法樹(AST)。
typedef struct _node {
short n_type;
char *n_str;
int n_lineno;
int n_col_offset;
int n_nchildren;
struct _node *n_child;
int n_end_lineno;
int n_end_col_offset;
} node;
由於CST可能是語法,令牌ID或者符號樹,因此編譯器很難根據Python語言做出快速決策。
這就是下一階段將CST轉換為更高層次結構的AST的原因。此任務由Python/ast.c
模塊執行,該模塊具有C版和Python API版本。在跳轉到AST之前,有一種方法可以從解析器階段訪問輸出。CPython有一個標准的庫模塊parser,它使用Python API去展示C函數的內容。該模塊被記錄為CPython的實現細節,因此你不會在其他Python解釋器中看到它。此外,函數的輸出也不容易閱讀。輸出將采用數字形式,使用make regen-grammar
階段生成的symbol和token編號,存儲在Includ/token.h
和Include/symbol.h
中。
>>> from pprint import pprint
>>> import parser
>>> st = parser.expr('a + 1')
>>> pprint(parser.st2list(st))
[258,
[332,
[306,
[310,
[311,
[312,
[313,
[316,
[317,
[318,
[319,
[320,
[321, [322, [323, [324, [325, [1, 'a']]]]]],
[14, '+'],
[321, [322, [323, [324, [325, [2, '1']]]]]]]]]]]]]]]]],
[4, ''],
[0, '']]
為了便於理解,你可以獲取symbol和token模塊中的所有數字,將它們放入字典中,並使用名稱遞歸替換parser.st2list()輸出的值。
import symbol
import token
import parser
def lex(expression):
symbols = {v: k for k, v in symbol.__dict__.items() if isinstance(v, int)}
tokens = {v: k for k, v in token.__dict__.items() if isinstance(v, int)}
lexicon = {**symbols, **tokens}
st = parser.expr(expression)
st_list = parser.st2list(st)
def replace(l: list):
r = []
for i in l:
if isinstance(i, list):
r.append(replace(i))
else:
if i in lexicon:
r.append(lexicon[i])
else:
r.append(i)
return r
return replace(st_list)
你可以使用簡單的表達式運行lex(),例如a+ 1,查看它如何表示為解析器樹:
>>> from pprint import pprint
>>> pprint(lex('a + 1'))
['eval_input',
['testlist',
['test',
['or_test',
['and_test',
['not_test',
['comparison',
['expr',
['xor_expr',
['and_expr',
['shift_expr',
['arith_expr',
['term',
['factor', ['power', ['atom_expr', ['atom', ['NAME', 'a']]]]]],
['PLUS', '+'],
['term',
['factor',
['power', ['atom_expr', ['atom', ['NUMBER', '1']]]]]]]]]]]]]]]]],
['NEWLINE', ''],
['ENDMARKER', '']]
在輸出中,你可以看到小寫的符號(symbols),例如'test'和大寫的標記(tokens),例如'NUMBER'。
抽象語法樹
CPython解釋器的下一個階段是將解析器生成的CST轉換為可以執行的更合理的結構。
該結構是代碼的更高級別表示,稱為抽象語法樹(AST)。
AST是使用CPython解釋器進程內聯生成的,但你也可以使用標准庫中的ast
模塊以及C API在Python中生成它們。
在深入研究AST的C實現之前,理解一個簡單的Python代碼的AST是很有用的。
為此,這里有一個名為instaviz的簡單應用程序。可以在Web UI中顯示AST和字節碼指令(稍后我們將介紹)。
小插曲
這里我需要說下,因為我按照原文的例子去照着做,發現根本就運行不起來,所以我就和大家說我的做法。
首先,我們不能通過pip的方式去安裝運行,而是從github上把他的源碼下載下來,然后在其文件下創建一個文件。
該程序需要在Python3.6+的環境下運行,包含3.6。
1.下載
https://github.com/tonybaloney/instaviz.git
2.寫腳本
隨意命名,比如example.py,代碼如下
import instaviz
def example():
a = 1
b = a + 1
return b
if __name__ == "__main__":
instaviz.show(example)
3.目錄結構如下
4.修改文件web.py
將原來的server_static函數和home函數用下面的代碼替換
@route("/static/<filename>")
def server_static(filename):
return static_file(filename, root="./static/")
@route("/", name="home")
@jinja2_view("home.html", template_lookup=["./templates/"])
def home():
global data
data["style"] = HtmlFormatter().get_style_defs(".highlight")
data["code"] = highlight(
"".join(data["src"]),
PythonLexer(),
HtmlFormatter(
linenos=True, linenostart=data["co"].co_firstlineno, linespans="src"
),
)
return data
5.運行
好了,現在可以運行example.py文件了,運行之后會生成一個web服務(因為這個模塊是基於bottle框架的),然后瀏覽器打開
http://localhost:8080/
6.展示頁面
好了,我們繼續原文的思路。
這里就到了展示圖了
左下圖是我們聲明的example函數,表示為抽象語法樹。
樹中的每個節點都是AST類型。它們位於ast模塊中,繼承自_ast.AST。
一些節點具有將它們鏈接到子節點的屬性,與CST不同,后者具有通用子節點屬性。
例如,如果單擊中心的Assign節點,則會鏈接到b = a + 1行:
它有兩個屬性:
- targets是要分配的名稱列表。它是一個列表,因為你可以使用解包來使用單個表達式分配多個變量。
- value是要分配的值,在本例中是BinOp語句,a+ 1。
如果單擊BinOp語句,則會顯示相關屬性:
left:運算符左側的節點
op:運算符,在本例,是一個Add節點(+)
right:運算符右側的節點
看一下圖就了解了
。
在C中編譯AST並不是一項簡單的任務,因此Python/ast.c
模塊超過5000行代碼。
有幾個入口點,構成AST的公共API的一部分。
在詞法分析(Lexing)和句法分析(Parsing)的最后一節中,我們講到了對PyAST_FromNodeObject()
的調用。在此階段,Python解釋器進程以node * tree的格式創建了一個CST。然后跳轉到Python/ast.c
中的PyAST_FromNodeObject()
,你可以看到它接收node * tree,文件名,compiler flags和PyArena。
此函數的返回類型是定義在文件Include/Python-ast.h
的mod_ty函數。
mod_ty是Python中5種模塊類型之一的容器結構:
1.Module
2.Interactive
3.Expression
4.FunctionType
5.Suite
在Include/Python-ast.h
中,你可以看到Expression類型需要一個expr_ty類型的字段。expr_ty類型也是在Include/Python-ast.h
中定義。
enum _mod_kind {Module_kind=1, Interactive_kind=2, Expression_kind=3,
FunctionType_kind=4, Suite_kind=5};
struct _mod {
enum _mod_kind kind;
union {
struct {
asdl_seq *body;
asdl_seq *type_ignores;
} Module;
struct {
asdl_seq *body;
} Interactive;
struct {
expr_ty body;
} Expression;
struct {
asdl_seq *argtypes;
expr_ty returns;
} FunctionType;
struct {
asdl_seq *body;
} Suite;
} v;
};
AST類型都列在Parser/Python.asdl
中,你將看到所有列出的模塊類型,語句類型,表達式類型,運算符和結構。本文檔中的類型名稱與AST生成的類以及ast標准模塊庫中指定的相同類有關。
Include/Python-ast.h
中的參數和名稱與Parser/Python.asdl
中指定的參數和名稱直接相關:
-- ASDL's 5 builtin types are:
-- identifier, int, string, object, constant
module Python
{
mod = Module(stmt* body, type_ignore *type_ignores)
| Interactive(stmt* body)
| Expression(expr body)
| FunctionType(expr* argtypes, expr returns)
因為C頭文件和結構在那里,因此Python/ast.c
程序可以快速生成帶有指向相關數據的指針的結構。查看PyAST_FromNodeObject()
,你可以看到它本質上是一個switch語句,根據TYPE(n)的不同作出不同操作。TYPE()是AST用來確定具體語法樹中的節點是什么類型的核心函數之一。在使用PyAST_FromNodeObject()的情況下,它只是查看第一個節點,因此它只能是定義為Module,Interactive,Expression,FunctionType的模塊類型之一。TYPE()的結果要么是符號(symbol)類型要么是標記(token)類型。
對於file_input,結果應該是Module。Module是一系列語句,其中有幾種類型。
遍歷n的子節點和創建語句節點的邏輯在ast_for_stmt()
內。如果模塊中只有1個語句,則調用此函數一次,如果有多個語句,則調用循環。然后使用PyArena返回生成的Module。
對於eval_input,結果應該是Expression,CHILD(n,0)
(n的第一個子節點)的結果傳遞給ast_for_testlist()
,返回expr_ty類型。然后使用PyArena將此expr_ty發送到Expression()以創建表達式節點,然后作為結果傳回:
mod_ty
PyAST_FromNodeObject(const node *n, PyCompilerFlags *flags,
PyObject *filename, PyArena *arena)
{
...
switch (TYPE(n)) {
case file_input:
stmts = _Py_asdl_seq_new(num_stmts(n), arena);
if (!stmts)
goto out;
for (i = 0; i < NCH(n) - 1; i++) {
ch = CHILD(n, i);
if (TYPE(ch) == NEWLINE)
continue;
REQ(ch, stmt);
num = num_stmts(ch);
if (num == 1) {
s = ast_for_stmt(&c, ch);
if (!s)
goto out;
asdl_seq_SET(stmts, k++, s);
}
else {
ch = CHILD(ch, 0);
REQ(ch, simple_stmt);
for (j = 0; j < num; j++) {
s = ast_for_stmt(&c, CHILD(ch, j * 2));
if (!s)
goto out;
asdl_seq_SET(stmts, k++, s);
}
}
}
/* Type ignores are stored under the ENDMARKER in file_input. */
...
res = Module(stmts, type_ignores, arena);
break;
case eval_input: {
expr_ty testlist_ast;
/* XXX Why not comp_for here? */
testlist_ast = ast_for_testlist(&c, CHILD(n, 0));
if (!testlist_ast)
goto out;
res = Expression(testlist_ast, arena);
break;
}
case single_input:
...
break;
case func_type_input:
...
...
return res;
}
在ast_for_stmt()函數里,也有一個switch語句,它會判斷每個可能的語句類型(simple_stmt,compound_stmt等),以及用於確定節點類的參數的代碼。
再來一個簡單的例子,2**4
2的4次冪。這個函數首先得到ast_for_atom_expr(),這是我們示例中的數字2,然后如果有一個子節點,則返回原子表達式.如果它有多個字節點,使用Pow操作符之后,左節點是一個e(2),右節點是一個f(4)。
static expr_ty
ast_for_power(struct compiling *c, const node *n)
{
/* power: atom trailer* ('**' factor)*
*/
expr_ty e;
REQ(n, power);
e = ast_for_atom_expr(c, CHILD(n, 0));
if (!e)
return NULL;
if (NCH(n) == 1)
return e;
if (TYPE(CHILD(n, NCH(n) - 1)) == factor) {
expr_ty f = ast_for_expr(c, CHILD(n, NCH(n) - 1));
if (!f)
return NULL;
e = BinOp(e, Pow, f, LINENO(n), n->n_col_offset,
n->n_end_lineno, n->n_end_col_offset, c->c_arena);
}
return e;
}
如果使用instaviz模塊查看上面的函數
>>> def foo():
2**4
>>> import instaviz
>>> instaviz.show(foo)
在UI中,你還可以看到其相應的屬性:
總之,每個語句類型和表達式都是由一個相應的ast_for_*()函數來創建它。
參數在Parser/Python.asdl
中定義,並通過標准庫中的ast模塊公開出來。
如果表達式或語句具有子級,則它將在深度優先遍歷中調用相應的ast_for_*
子函數。
結論
CPython的多功能性和低級執行API使其成為嵌入式腳本引擎的理想候選者。
你將看到CPython在許多UI應用程序中使用,例如游戲設計,3D圖形和系統自動化。
解釋器過程靈活高效,現在你已經了解它的工作原理。
在這一部分中,我們了解了CPython解釋器如何獲取輸入(如文件或字符串),並將其轉換為邏輯抽象語法樹。我們還沒有處於可以執行此代碼的階段。接下來,我們將繼續深入,了將抽象語法樹轉換為CPU可以理解的一組順序命令的過程。
-后續-
更多技術內容,關注公眾號:python學習開發