文中的Python程序的CST看起来大致是这样的:

图中省略了很多细节,因为是CST的关系,分析过程中有大量冗余信息,主要是每一步通过DFA分析的结点都列在这棵树里面了。由于篇幅的关系就不把整棵树画出来了。
那么CST是怎么生成的呢?在PyParser_ParseFileFlags中:
|
node * PyParser_ParseFileFlags(FILE *fp, const char *filename, grammar *g, int start, char *ps1, char *ps2, perrdetail *err_ret, int flags) { struct tok_state *tok;
initerr(err_ret, filename);
if ((tok = PyTokenizer_FromFile(fp, ps1, ps2)) == NULL) { err_ret->error = E_NOMEM; return NULL; } tok->filename = filename; if (Py_TabcheckFlag || Py_VerboseFlag) { tok->altwarning = (filename != NULL); if (Py_TabcheckFlag >= 2) tok->alterror++; }
return parsetok(tok, g, start, err_ret, flags); } |
PyParser_ParserFileFlags首先创建tok_state,也就是词法分析器的对象,之后调用parsetok。Parsetok的代码量稍多,这里就不全部列出来了。主干代码如下:
|
static node * parsetok(struct tok_state *tok, grammar *g, int start, perrdetail *err_ret, int flags) { parser_state *ps; node *n; int started = 0, handling_import = 0, handling_with = 0;
ps = PyParser_New(g, start);
for (;;) { type = PyTokenizer_Get(tok, &a, &b); if ((err_ret->error = PyParser_AddToken(ps, (int)type, str, tok->lineno, col_offset, &(err_ret->expected))) != E_OK) { if (err_ret->error != E_DONE) { PyObject_FREE(str); err_ret->token = type; } break; } }
if (err_ret->error == E_DONE) { n = ps->p_tree; ps->p_tree = NULL; } else n = NULL;
PyParser_Delete(ps);
if (n == NULL) { // error processing } else if (tok->encoding != NULL) { node* r = PyNode_New(encoding_decl); if (!r) { err_ret->error = E_NOMEM; n = NULL; goto done; } r->n_str = tok->encoding; r->n_nchildren = 1; r->n_child = n; tok->encoding = NULL; n = r; }
done: PyTokenizer_Free(tok);
return n; } |
里面最重要的是两个函数调用:
- PyTokenizer_Get,是用来进行词法分析的,把源程序分解为Token的序列(比如变量名,运算符,关键字等都属于Token)
- PyParser_AddToken,把Token加入到DFA状态机中进行语法分析,根据当前的状态和输入的Token,根据跳转表跳转到不同的状态,并依照这个过程生成CST
由于篇幅有限,这个两个函数的实现细节会在以后的文章详细分析。
在PyParser_ParserFileFlags得到了语法树之后,PyAST_FromNode会将CST转换为AST,存入mod_ty中。
|
typedef _mod *mod_ty;
struct _mod { enum _mod_kind kind; union { struct { asdl_seq *body; } Module;
struct { asdl_seq *body; } Interactive;
struct { expr_ty body; } Expression;
struct { asdl_seq *body; } Suite;
} v; }; |
_mod是AST的根结点,代表整个Module。在Python-ast.h中定义着所有AST结点的结构,mod_ty和expr_ty也在其中,后者显然代表着一个表达式。
adsl_seq代表一个变长的指针数组,结构定义如下:
|
typedef struct { int size; void *elements[1]; } asdl_seq; |
这个结构稍微有些特殊的是elements在struct中只有一个元素。其实这个struct可以支持任意多个元素,正因为如此,普通的定义方法是不行的。因此这里只定义一个元素,然后在分配计算实际的大小(会比adsl_seq这个结构要大),然后访问元素的时候直接用elements,因为C/C++是不会检查越界的。这种做法在C/C++系统编程中还是比较常见的。
Adsl_seq粗看上去只保存了void *,也就是说具体类型信息丢失了,那么当要遍历整个树的时候是如何做的呢?其实当遍历到某个结点(比如Module/Expression)的时候,便可以确定该结点所支持的子结点是什么类型,然后直接转强制转换就可以了。还有一种情况是在结点中直接记录的是具体的有类型的AST结点,比如expr_ty,就更加容易了。
文中Python代码对应的AST大致如下:

同样的,也省略了一些细节。可以看到,最终的结果要比CST要少很多,比较接近源代码本来的样子。
当PyParser_ASTFromFile执行完毕之后,回到PyRun_FileExFlags,执行下一步,调用run_mod,也就是执行这颗AST所代表的程序。
|
static PyObject * run_mod(mod_ty mod, const char *filename, PyObject *globals, PyObject *locals, PyCompilerFlags *flags, PyArena *arena) { PyCodeObject *co; PyObject *v; co = PyAST_Compile(mod, filename, flags, arena); if (co == NULL) return NULL; v = PyEval_EvalCode(co, globals, locals); Py_DECREF(co); return v; } |
Run_mod非常直接,分两步走:
- 编译AST,生成PyCodeObject对象,也就是ByteCode。
- 调用PyEval_EvalCode执行PyCodeObject,也就是通过VM直接执行bytecode。如果是执行.pyc/.pyo代码的话,直接从文件中读出信息创建好PyCodeObject对象就可以直接执行了,也是调用这个函数。
执行完PyEval_EvalCode之后可以看到55被打印出来了。
总结一下,Python执行程序的过程总共有以下几步:
- Tokenizer进行词法分析,把源程序分解为Token
- Parser根据Token创建CST
- CST被转换为AST
- AST被编译为字节码
- 执行字节码
后面的文章中我会对1~5这5步分别进行详细分析,今天就先写到这里。







