# 函数调用与虚拟机软件栈
# 准备代码
准备文件geometry.py :
# geometry.py
pi = 3.14
def circle_area(r):
return pi * r ** 2
def cylinder_volume(r, h):
return circle_area(r) * h
注意,文件中定义有两个函数,其中计算圆柱体体积的函数将调用计算圆面积的函数。
准备main.py
# main.py
from geometry import circle_area, cylinder_volume
circle_area(1.5)
# import
开始讨论函数调用流程之前,我们先来看看从 geometry 模块导入相关函数后虚拟机内部的状态,即执行完下面第一行指令后的状态:
from geometry import circle_area, cylinder_volume
main 模块是 Python 启动后的执行入口,每个 Python 程序均从 main 开始执行;
geometry 是我们导入的模块,它有一个 dict 属性,指向模块属性空间;
geometry 初始化后,属性空间里有一个浮点属性 pi 以及两个函数对象, circle_area 和 cylinder_colume ;
两个函数的 全局名字空间 与模块对象的 属性空间 是同一个 dict 对象;
本质上是由于在在执行geometry code的frame中make的function - make出来的function和其出生所在的frame的globals保持一致
两个函数都有一个 代码对象 ,保存函数 字节码 以及 名字 、 常量 等静态上下文信息;
# 函数调用
# 单层调用
当解释器执行到第二行即circle_area(1.5)时,Python 创建栈帧对象作为执行环境,准备执行编译后的代码对象:
注意到,栈帧对象全局名字空间、局部名字空间均指向 main 模块的属性空间。 circle_area(1.5) 的语句中第一条字节码将名为 circle_area 的对象加载到栈顶-这是要调用的函数;第二条字节码将常量 1.5 加载到栈顶-这是准备传递给函数的变量。
执行这两个字节码后,虚拟机状态变为:
接着是 CALL_FUNCTION 字节码,顾名思义,其正式完成调动函数的使命:
CALL_FUNCTION 先创建一个新栈帧对象,作为 circle_name 函数的执行环境。新栈帧对象通过 f_back 指针,指向前一个栈帧对象,形成一个调用链。栈帧对象从函数对象取得 代码 对象,以及执行函数时的全局名字空间:
注意执行函数的栈帧对象 f_locals 字段为空,而不是跟 f_globals 一样指向一个 dict 对象。由于函数有多少局部变量是固定的,代码编译时就能确定。因此,没有必要用字典来实现局部名字空间,只需把局部变量依次编号,保存在栈底即可 ( r=1.5 处)。这样一来,通过编号即可快速存取局部变量,效率比字典更高。于此对应,有一个特殊的字节码 LOAD_FAST 用于加载局部变量,以操作数的编号为操作数。
最后, RETURN_VALUE 字节码将结算结果返回给调用者,执行权现在交回调用者的 CALL_FUNCTION 字节码。CALL_FUNCTION 先将结果保存到栈顶并着手回收 circle_area 函数的栈帧对象。
# 嵌套调用
嵌套调用也是类似的,以 cylinder_volume(1.5, 2) 为例:
>>> cylinder_volume(1.5, 2)
14.13
Python 交互式终端同样先对这个语句进行编译,得到这样的字节码:
1 0 LOAD_NAME 0 (cylinder_volume)
2 LOAD_CONST 0 (1.5)
4 LOAD_CONST 1 (2)
6 CALL_FUNCTION 2
8 PRINT_EXPR
10 LOAD_CONST 2 (None)
12 RETURN_VALUE
然后, Python 虚拟机以 main 栈帧对象为环境,执行这段字节码。当虚拟机执行到 CALL_FUNCTION 这个字节码时,创建新栈帧对象,准备执行函数调用。初始新栈帧对象时,函数参数来源于当前栈顶,而全局名字空间与代码对象来源于被调用函数对象。新栈帧对象初始化完毕,虚拟机便跳到新栈帧,开始执行 cylinder_volume 的字节码。cylinder_volume 字节码中也有 CALL_FUNCTION 指令,调用 circle_area 函数。虚拟机依样画葫芦,为 circle_area 准备栈帧,并开始执行 circle_area 的字节码:
这样一来,随着函数调用的深入,栈帧链逐渐伸长;随着函数执行完毕并返回,栈帧链逐渐收缩。维护栈帧链条的关键是栈帧对象的 f_back 指针,它总是指向上个一栈帧对象,也就是调用者的栈帧,如上图红色箭头。我们在调试程序时,可以查看完整的堆栈信息,也是 f_back 指针的功劳。