本次实战内容是受到Javascript的启发,将Python为人诟病已久的lambda函数改成Javascript风格的箭头函数,效果如下:
上一章讲到遍历AST树并生成符号表,本章讲解析AST树以及完成最终的字节码生成。
11. 在Python/compile.c
文件第3034行添加如下代码:
static int
compiler_arrowlbd(struct compiler *c, expr_ty e) {
PyCodeObject *co;
Py_ssize_t funcflags;
arguments_ty args = e->v.ArrowLbd.args;
assert(e->kind == ArrowLbd_kind);
RETURN_IF_ERROR(compiler_check_debug_args(c, args));
location loc = LOC(e);
funcflags = compiler_default_arguments(c, loc, args);
if (funcflags == -1) {
return ERROR;
}
_Py_DECLARE_STR(anon_lambda, "<lambda>");
RETURN_IF_ERROR(
compiler_enter_scope(c, &_Py_STR(anon_lambda), COMPILER_SCOPE_LAMBDA,
(void *)e, e->lineno));
/* Make None the first constant, so the lambda can't have a
docstring. */
RETURN_IF_ERROR(compiler_add_const(c->c_const_cache, c->u, Py_None));
c->u->u_metadata.u_argcount = asdl_seq_LEN(args->args);
c->u->u_metadata.u_posonlyargcount = asdl_seq_LEN(args->posonlyargs);
c->u->u_metadata.u_kwonlyargcount = asdl_seq_LEN(args->kwonlyargs);
VISIT_IN_SCOPE(c, expr, e->v.ArrowLbd.body);
if (c->u->u_ste->ste_generator) {
co = optimize_and_assemble(c, 0);
}
else {
location loc = LOCATION(e->lineno, e->lineno, 0, 0);
ADDOP_IN_SCOPE(c, loc, RETURN_VALUE);
co = optimize_and_assemble(c, 1);
}
compiler_exit_scope(c);
if (co == NULL) {
return ERROR;
}
if (compiler_make_closure(c, loc, co, funcflags) < 0) {
Py_DECREF(co);
return ERROR;
}
Py_DECREF(co);
return SUCCESS;
}
并在6156行添加如下代码:
case ArrowLbd_kind:
return compiler_arrowlbd(c, e);
生成符号表后需要再次遍历AST树,本次遍历是根据树节点生成字节码。
字节码是Python编译后生成的中间表示(IR),是最小的不可分割的执行单元。和汇编语言一样,字节码不涉及循环和选择,所有的循环和选择都由跳转指令完成。在第一次编译Python文件后会生成.pyc
文件,该文件存放刚编译好的字节码以便下次执行。每个版本的Python会使用不同的字节码,本实战采用3.11的字节码。
在compiler.c
中,通过以下几个宏实现字节码的发射(emit):
ADDOP(struct compiler *, int)
: 添加一个字节码,字节码用整形表示ADDOP_NOLINE(struct compiler *, int)
: 添加一个字节码,但是这个字节码没有行号,用于跳转ADDOP_IN_SCOPE(struct compiler *, int)
: 在scope内添加一个字节码但不进入该scope,scope的概念等同于符号表的blockADDOP_I(struct compiler *, int, Py_ssize_t)
: 添加一个带int参数的字节码ADDOP_O(struct compiler *, int, PyObject *, TYPE)
: 添加一个带PyObject参数的字节码ADDOP_LOAD_CONST(struct compiler *, PyObject *)
: 添加LOAD_CONST
字节码ADDOP_JUMP(struct compiler *, int, basicblock *)
: 添加直接跳转字节码,跳转到basicblock
ADDOP_JUMP_COMPARE(struct compiler *, cmpop_ty)
: 添加比较跳转字节码,被比较的值为栈顶的值
生成好的字节码会被暂存到内会被暂存到compiler
的compiler_unit
的instr_sequence
内,最终通过assemble
生成PyCodeObject
对象。字节码生成阶段也有类似block的概念,叫scope。当出现函数调用、lambda函数调用、进入class等场合会进入scope。分别通过compiler_enter_scope
和compiler_exit_scope
实现进入和离开scope。
本步骤的代码是箭头函数的字节码生成逻辑。compiler_arrowlbd
函数的入口通过递归调用宏VISIT
调用。该函数先处理默认参数,然后再进入scope,再处理generator的情况。最后在离开scope之前先编译生成scope内的字节码。
12. 再次在命令行中运行PCBuild/build.bat
生成全可执行文件
在最终的python程序中输入箭头函数表达式可以看到该python程序可以正常的处理箭头函数。此外,由于在语法分析文件中复用了params表达式,所以该箭头函数还具备type hint能力。
到此为止,本实战就告一段落了,但是只是用最少的知识给Python新增了一个语法特性,还有很多细节有待探索,比如cpython是如何把上述步骤都串起来的,字节码是如何解析的等等。
如果要进一步探索,我建议可以结合devguide直接阅读cpython源码,或者找一些国内外技术博客作为辅助。我也会在后续的博客中分享cpython相关内容。