首页 > Python基础教程 >
-
教你阅读 Cpython 的源码(二)(2)
在深入研究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学习开发