函数是程序重要的抽象. 在 Python 中使用 PyFrameObject 实现栈帧, 它们构成栈帧链.
对函数的表示内部使用此对象:
struct PyFunctionObject { HEAD 部分; PyObject *func_code; // 对应编译后的 PyCodeObject 对象. PyObject *func_globals; // 函数运行时的 global 名字空间 PyObject *func_defaults; // 默认参数 PyObject *func_closure; // 用于实现闭包. PyObject *func_doc; // string 函数文档. PyObject *func_name; // string 函数名称. PyObject *func_dict; // 函数的 __dict__ 属性. ... PyObject *func_module; // 函数的 __module__ }
对象 PyCodeObject 与 PyFunctionObject 都与函数有关, 但区别很重要. code 是代码的静态表示, 一个 python
代码块编译后对应一个且仅一个 code 对象.
PyFunctionObject 则不同, 它是 python 运行时动态产生的, 更准确地说, 是在执行 def 语句时创建的. 此时函数
的静态代码即放在 func_code 字段中. 除此之外还包含一些函数在执行时需要的(动态)信息, 如 globals 等.
对于一段 python 代码, 其对应的 PyCodeObject 对象只有一个, 而产生的 PyFunctionObject 对象却可能有多个.
无参函数是最简单的函数调用形式, 其不用考虑参数传递机制问题.
// 实验用代码 def f(): 0 LOAD CONST 0 (code object f) 3 MAKE_FUNCTION 0 6 STORE_NAME 'f' # 关联名字 f->code_object print "I'm func" 0 LOAD_CONST "I'm func" 3 PRINT_ITEM ... f() // 调用函数 f 9 LOAD_NAME 'f' 12 CALL_FUNCTION 0 15 ...
上面的实验代码中, 使用 def f() 定义了一个函数 f(), 共产生两个 PyCodeObject, 主程序一个, 函数 f 的体一个.
这里 def f() 语句是一个函数声明语句, 同时在虚拟机角度也是函数对象(PyFunctionObject) 的创建语句.
// 虚拟机指令 swtich case MAKE_FUNCTION: code = POP(); // 得到为函数 f 构建的 PyCodeObject 对象 x = PyFunction_New(code, 当前栈帧 globals 对象); // 构造 PyFunctionObject 对象 ...
注意这里创建的新函数对象使用当前栈帧的 globals 对象. 函数对象建立后, 在 local 名字空间建立起 f->FuncObj
的关联关系.
函数调用的指令是 CALL_FUNCTION:
case CALL_FUNCTION: PyObject **sp = stack_pointer; // 栈指针 x = call_function(&sp, oparg); stack_pointer = sp; // 恢复栈指针. ... // 函数 call_function @ ceval.c PyObject *call_function(PyObject ***pp_stack, int oparg) { // 处理各种参数等, 略 // 获得要执行的 PyFunctionObject 对象 PyObject *pfunc = *pp_stack[计算出的正确位置]; // 检查 pfunc 是函数, 略 // 实际调用 x = fast_function|do_call(func, ...); ... }
函数快速调用通道:
PyObject *fast_function(PyObject *func, ...) { PyCodeObject *code = func->func_code; // 不相关的暂时略 PyThreadState *thread = Py_CurrentThread(); PyFrameObject *frame = new PyFrameObject(thread, code, ...); ... var retval = PyEval_EvalFrameEx(frame, ...) ... return retval; }
这里的关键是创建新的栈帧 frame, 然后(递归)调用 evalxxx() 函数. 另外的执行路径追根究底也是最终调用 eval.
而这里 eval 我们已经知道是函数调用过程的实现: 创建新的栈帧, 在新的栈帧中执行代码.
Python 使用 LGB 规则搜索名字空间. PyFunctionObject 会绑定一个 globals 名字空间, 该 globals 在函数
执行中被作用 globals 来搜索.
当创建函数, 即绑定 f=code 对象时, 在 py 文件层 globals=locals 名字空间, 所以将名字 f=code 放入 locals
名字空间即等于放入 globals 名字空间. (一点点小技巧)
重点放在参数的传递机制上, 同时剖析局部变量实现方式.
Python 支持四种类别的参数:
1. 位置参数 (positional argument): f(a,b) -- a,b 是位置参数
2. 键参数 (key argument): f(name='python') -- name 是键参数
3. 扩展位置参数 (excess positional argument): def f(a, b, *list) -- *list 是扩展位置参数
4. 扩展键参数 (excess key argument): def (a, b, **keys) -- **keys 是扩展键参数
由于扩展xx参数是xx参数的更高级形式, 实际只需要记录位置参数的个数, 以及键参数的个数, 就能知道一共有多少
个参数, 共需要多少内存来存放参数. (这里可与 lisp 的参数传递方式对比, 很相似...)
下面分别研究几种参数对应的 co_nlocals, co_argcount 值, 以及关系...
def foo(a, b): # 两个位置参数 a,b. pass foo(1,2) # 调用该函数 # hack 打印出 na=2(位置参数数量), nk=0(键参数数量) # co_argcount=2(共两个参数), co_nlocals=2(局部变量区含两个参数)
位置参数+键参数:
def foo(a, b): pass foo(1, b=2) # 以键参数 b=2 调用 # hack 打印出 na=1(位置参数1个), nk=1(键参数1个), n=3
对照两个例子, 作者结论是 Python 中函数参数中确定一个参数是位置参数, 还是键参数, 实际上仅仅由(调用处)
给出的函数实参的形式所决定, 和定义时形参无关. (这一点和静态语言如 C, Java 不同). na(num argu) 和 nk
(num key_arg) 忠实地反映着(调用者给出的)位置参数和键参数的个数.
那么下面的问题是, 当调用者给出了位置参数,键参数之后, 如何绑定到函数定义的形参上?
我猜 na (位置实参数量)绑定到位置形参上, 多的变成扩展形参 *list, 少的补上 null/nil;
同样猜 nk (键实参) 绑定到键参数(含位置参数)上, 多余的变成扩展形参 **key, 少的补上值 null/nil.
作者猜测, python 实现中所有的扩展位置参数实际上是被存储在了一个 PyListObject 对象中.
(对比: lisp 中是存为 &rest/&body 列表......)
在调用 call_function(pp_stack, oparg) 时, oparg 两个字节分别给出位置实参, 键实参数量.
PyObject *call_function(Py_Object ***pp_stack, int oparg) { int na = oparg & 0xFF; // 位置实参数量. int nk = oparg >> 8; // 键实参数量. int n = na + 2 * nk; ... }
计算的 n = na+2*nk 我猜是每位置实参占用1个栈空间, 每一个键实参占用2个栈空间(key,value是两个值).
根据 na, nk 以及记录在 PyCodeObject 中 nargcount(?也含nkeycount), 能够计算出需要绑定的形参
数量(差).
// (猜测) 当只有位置参数时的快速函数调用处理 PyObject *fast_function(PyObject *func, pp_stack, n, na, nk) { ... PyFrame_Object *frame = new PyFrame_Object(...); // 拷贝函数参数: for (i = 0 ; i < n; ++i) locals[i] = *stack[i]; ... }
为访问局部变量, Python 虚拟机提供的指令对为 LOAD_FAST/STORE_FAST. (可认为是读/写栈中变量)
位置参数的缺省值, 如 def f(a=1, b=2) 被放置到 PyFunctionObject.func_defaults 字段中. 在有缺省值
的情况下, 函数调用不走 fast_function() 快速调用通道.
而是走 PyEval_EvalCodeEx() 通道. 这里有大堆代码, 我们简略一点:
PyObject *PyEval_EvalCodeEx(PyCodeObject *code, PyObject *globals, PyObject *locals, // 作用域 PyObject **args, int argcount, // 位置参数信息 PyObject **kws, int kwcount, // 键参数信息 PyObject **defs, int defcount, // 函数默认参数信息 PyObject *closure) { ... }
这里仅从参数我们能看出, 此函数将负责绑定位置实参->键形参, 以及键实参->键形参, 以及处理位置实参如果未
给出时的默认值绑定. (细节看代码吧, 机理了解即可).
由于看起来绑定 key 参数需要复杂的遍历查找和构造 dict/list? 的工作, 也许考虑到性能因素的函数, 就尽量
少使用 key 来传递参数, 代之以位置参数或者数组或者dict 也许是一个选择.
关于扩展位置参数和扩展键参数, 我想大致也是这样. 只是代码要写复杂一些...
如果函数有扩展位置参数, 则设置 flags CO_VARARGS; 扩展键标志 CO_VARKEYWORDS.
略看了一下, 扩展位置参数放入到了 PyTupleObject 中. tuple 按照 python 介绍是一种不可变长度的 list.
扩展键参数放入 dict 中 (PyDict_New)... 注意:成本不低的...
前面述及, 函数参数实际上也是一种局部的变量(实参->形参绑定之后), 都是通过 LOAD_FAST/STORE_FAST
指令读写(位于栈中). 由于编译期能够确定局部变量数量, 所以没有必要使用 locals 名字空间做动态搜索.
不然效率太低下.
示例嵌套函数, 实际为一个闭包:
def get_compare(base): # 此函数返回另一个函数 def real_compare(value): return value > base # 使用外部词法域变量 base. return real_compare # 返回闭包函数 compare_with_10 = get_compare(10) # 相当于定义新函数 compare_with_10 compare_with_10(5) # 调用...
因此为支持嵌套函数实际上需要实现闭包. 也是实现在 PyFunctionObject 中.
在 PyCodeObject 中有两个相关字段:
1. co_cellvars: 保存被嵌套的作用域中使用的变量名集合 (该变量可能会被闭包).
2. co_freevars: 保存使用了的外层作用域中的变量名集合 (将闭包的外层的变量).
分配 frame 栈帧时计算所需栈空间的公式中含有这些值:
extras = code->co_stacksize + code->co_nlocals + ncells + nfrees; // 后两者即上面 co_...
为实验闭包实现的 python 代码:
def get_func(): # 1.外层函数 value = 'inner' # 2.将被(内层函数)闭包的变量 def inner_func(): # 3.内层函数 6 LOAD_CLOSURE 0 (value) 9 BUILD_TUPLE 1 12 LOAD_CONST 2 (Code: inner-func) 15 MAKE_CLOSURE 0 18 STORE_FAST 0 (inner-func) print value # 4.使用外层函数的变量, 也即闭包了 value 变量 0 LOAD_DEREF 0 (value) ... return inner_func # 5. f = get_func() # 6.得到闭包(函数) f() # 7.调用该闭包
为此引入一种 cell 对象 -- PyCellObject:
struct PyCellObject { HEAD 部分; PyObject *ob_ref; // content of the cell or NULL when empty }
看起来当一个会被闭包的局部变量(如例子中的 value) 实现为 PyCellObject, 其实际引用到的对象才是值.
指令 STORE_DEREF, LOAD_DEREF 用于访问这种被 cell 包装起来的值.
指令 LOAD_CLOSURE 用于获取 cell 对象本身, 建立起 tuple 存放在 PyFunctionObject 对象中, 这当然
应是在指令 MAKE_CLOSURE 中实现的了.
case LOAD_CLOSURE: x = freevars[oparg]; // freevars 存放会被闭包的变量 PUSH(x); ... case MAKE_CLOSURE: func = new PyFunction() closure_tuple = POP(); // 闭包的变量集合 func->SetClosure(closure_tuple) // 绑定约束集合 ...
这样在内部 func (即闭包函数)中, 即通过访问 closure_tuple 中保存的 cell 对象, 即访问到被闭包的外部变量
(如 value). 这是通过间接一层引用的方式来访问闭包变量, 所以这种闭包变量可以读, 也可以写. 闭包变量(的值)
可以在多个函数间共享. 当然也可以优化一些的, 对那些只读的, 或只有一家访问的...
在 closure 的基础上, python 实现了 decorator. (对 python 的这一语法知识不熟悉, 以后再补上......)
小结:
函数的重点(之一)是参数传递/绑定方式及其实现机制, 想支持的传递方式越多越灵活, 实现当然就越复杂.
为闭包的实现引入了新的 cell 对象, 我觉得可以把它当成指针理解...