python 内核分析(十一):Python 虚拟机的表达式

本系列作为本人@takooctopus深入学习python机制的记录,这个博客遵照着栖迟于一丘的博客上面的流程进行的,也包含我在实际查看源码时的感想,特此列出,表示感谢。

内建对象及其创建

我们定义一个简单的文件


{{- code -}}

我们使用下面的语句访问


{{- code -}}

最后我们得到最终的输出:


{{- code -}}

除开这些属性后,我们再去看这段代码的字节码:


{{- code -}}

上面就是foo.py所生成的字节码

可能用到的宏


{{- code -}}

指令解析

对应第一行


{{- code -}}

同样的,我们去[Python/ceval.c/_PyEval_EvalFrameDefault()]中寻找:


{{- code -}}

其上半句是取出常数的,GETITEM()相当于GETITEM(consts,1),并将这个对象压入时栈。

假设当前时栈对象PyFrameObjectfconsts就相当于f->f_code->co_consts

在压入时栈后,虚拟机就需要为local命名空间中创造一个与其相映射的符号i


{{- code -}}

我们可以看见names也就是f->f_code->co_names即符号表

nsf->f_locals是本地命名空间,而NAMESPACE都是一个dict对象,保存了一个变量和值的映射关系。


第二行对于字符串变量的赋值也差不多,仅仅是索引的变化


{{- code -}}

第三行l = [] 创建了一个list


{{- code -}}

其中调用的是BUILD_LIST


{{- code -}}

这里会新建一个list,并在如果参数为空的时候直接压入栈中,反之如果参数不为空,就会从栈中弹出元素,加入这个list。


第四行的d = {}会创建一个dict


{{- code -}}

我们看BUILD_MAP所对应的case


{{- code -}}

当执行完这一段Code Block后,python需要返回一些值


{{- code -}}

将其实际返回的值放在retval中,POP()从运行时栈中拉出的,实际值是上次LOAD_CONST 2 (None)的,即最后返回一个None值。最后跳转至fast_block_end中。


{{- code -}}

将虚拟机状态设置为WHY_RETURN,进入最后end阶段

复杂内建对象的创建

我们考虑建立一个新的非空list或者dict


{{- code -}}

我们同样的采用下面的代码查看:


{{- code -}}

我们查看结果:


{{- code -}}

我们可以看见常量表co_const和符号表co_names,注意常量表中的小整数是共用的,大整数不会。

以及其字节码


{{- code -}}

注意,在创建list时,即调用BUILD_LIST时传入参数2,之后会从栈帧中读出两个元素。

而创建dict时,会先将其值压入栈,再将其key压入栈,最后才创建dict

我们看创建函数BUILD_CONST_KEY_MAP


{{- code -}}

其他的一般表达式

这里继续使用了一段代码来看其实现:


{{- code -}}

{{- code -}}

以及其字节码


{{- code -}}

符号搜索

首先是一个LOAD_NAME这个指令,其会从符号表中取出变量值并压入栈中。


{{- code -}}

我们看到其采用了LGB原则去命名空间搜索变量。

数值运算

数值相加是指令BINARY_ADD,其会先将ab取出「LOAD_NAME」压入时栈,再进行加法


{{- code -}}

会判断左右类型,如果为字符串就进行字符串拼接,如果不是就进行PyNumber_Add进行数值相加。

信息输出

print()会先将printc压入栈,再调用CALL_FUNCTION,其参数为1


{{- code -}}

我们可以看见其模拟了一个CPU的过程,使用call_function调用

其中会进行类型检查等一系列动作,最后调用builtin_print()函数输出