一行简单的 Python 代码 print("hello world.") 被转换为 ByteCode,第一条指令是:
LOAD_NAME 0
0 是 “print” 在 names 中的索引。这条指令将 print 函数对象压入 VM 执行栈,我们估计下来该 “hello world.” 出场了,然后让我们一口气把剩下的指令处理完,拭目以待把。
压入 String Object
当 print 函数入栈之后,继续处理下一个 ByteCode,此时 opcode 为 100:
#define LOAD_CONST 100
显然,这是一个加载 CONST 的指令,对应的 oparg 是 0,所有的常量都在 consts 元组中,consts[0] 正是 "hello world.",一个 String Object。
// 查找 consts[0]
*value = GETITEM(consts, oparg);
// 增加引用计数
Py_INCREF(value);
// 压入栈
PUSH(value);
此时运行栈看起来是这样:
注意:实际上栈元素是 PyObject 指针,图示只是为了方便,将所有对象的内容显示在栈中。
调用函数
参数已经顺利的入栈了,下面继续下一个指令,opcode 为 131, oparg 为 1。
#define CALL_FUNCTION 131
oparg 表示函数的参数数量为 1,只有一个参数。此时栈中保存着函数调用所需要的函数对象、参数们。实际的函数调用由下面的函数承担:
// Python/ceval.c:4949
Py_LOCAL_INLINE(PyObject *) _Py_HOT_FUNCTION
call_function(PyThreadState *tstate, PyObject ***pp_stack, Py_ssize_t oparg, PyObject *kwnames)
{
// ··· ···
// 调用函数
x = _PyObject_Vectorcall(func, stack, nargs | PY_VECTORCALL_ARGUMENTS_OFFSET, kwnames);
// ··· ···
// 弹出函数对象和所有参数
while ((*pp_stack) > pfunc) {
w = EXT_POP(*pp_stack);
Py_DECREF(w);
}
return x;
}
CALL_FUNCTION 指令会首先执行调用栈上指定的函数,当 _PyObject_Vectorcall 返回的时候,函数就已经执行完毕,我们也得到了 “hello world.” 的输出;函数执行完后,立刻清理函数自身和所有参数。
特别注意函数的返回值 x,它是目标函数的 “返回值”,可以看到 Python 函数实际上只能返回 1 个对象,多个返回值实际上是通过其他技术实现的(打包为 tuple)。函数返回值 x 后面还有其他用处,这里也作为 call_function 的返回值,等待后续处理。
call_function 中执行了运行栈中指定的函数,并且返回了最终的返回值。print 函数的返回值是 None,这也正是 call_function 的返回值。随后 Python 立刻将返回值入栈,如果后面还有其他操作,就可以继续使用这个返回值了。
在此期间运行栈经过这样的变化:
后续就是 Python 的一些收尾工作,不再赘述,之后我们会更详细的分析每一步中发生的细节。
总结
至此,我们可以对 Python 的具体执行有了直观的了解:
我们不由的会萌生更多的疑问:
- Python 代码如何转换为 AST,
- CodeObject 是如何生成的?
- PyFrameObject 与 CodeObject 又有着怎样的关联,
- Python 的面向对象、闭包、协程、线程在最根本上是如何实现的?
- GPL 到底对 Python 有着怎样的意义。
每一个问题都直接决定于 Python 的本质是什么,搞定这些疑问,就算对 Python 有了相当的了解了。
北京城不是一天建成的,路还是要一步一步走,耐住性子一起继续努力吧。