likes
comments
collection
share

CPython开发实战:魔改lambda函数(四)

作者站长头像
站长
· 阅读数 24

本次实战内容是受到Javascript的启发,将Python为人诟病已久的lambda函数改成Javascript风格的箭头函数,效果如下:

CPython开发实战:魔改lambda函数(四)

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树,本次遍历是根据树节点生成字节码。

CPython开发实战:魔改lambda函数(四)

字节码是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的概念等同于符号表的block
  • ADDOP_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): 添加比较跳转字节码,被比较的值为栈顶的值

生成好的字节码会被暂存到内会被暂存到compilercompiler_unitinstr_sequence内,最终通过assemble生成PyCodeObject对象。字节码生成阶段也有类似block的概念,叫scope。当出现函数调用、lambda函数调用、进入class等场合会进入scope。分别通过compiler_enter_scopecompiler_exit_scope实现进入和离开scope。

本步骤的代码是箭头函数的字节码生成逻辑。compiler_arrowlbd函数的入口通过递归调用宏VISIT调用。该函数先处理默认参数,然后再进入scope,再处理generator的情况。最后在离开scope之前先编译生成scope内的字节码。

12. 再次在命令行中运行PCBuild/build.bat生成全可执行文件

在最终的python程序中输入箭头函数表达式可以看到该python程序可以正常的处理箭头函数。此外,由于在语法分析文件中复用了params表达式,所以该箭头函数还具备type hint能力。

CPython开发实战:魔改lambda函数(四)

到此为止,本实战就告一段落了,但是只是用最少的知识给Python新增了一个语法特性,还有很多细节有待探索,比如cpython是如何把上述步骤都串起来的,字节码是如何解析的等等。

如果要进一步探索,我建议可以结合devguide直接阅读cpython源码,或者找一些国内外技术博客作为辅助。我也会在后续的博客中分享cpython相关内容。