0x01 PyFunctionObject对象
typedef struct {
PyObject_HEAD
PyObject *func_code; /* 对应函数编译后的PyCodeObject对象 A code object */
PyObject *func_globals; /* 函数运行时的global名字空间 A dictionary (other mappings won't do) */
PyObject *func_defaults; /* 默认参数 NULL or a tuple */
PyObject *func_closure; /* 用于实现闭包 NULL or a tuple of cell objects */
PyObject *func_doc; /* 函数的文档(PyStringObject对象) The __doc__ attribute, can be anything */
PyObject *func_name; /* 函数的名称,函数的__name__属性 The __name__ attribute, a string object */
PyObject *func_dict; /* 函数的__dict__属性 The __dict__ attribute, a dict or NULL */
PyObject *func_weakreflist; /* List of weak references */
PyObject *func_module; /* 函数的__module__属性 The __module__ attribute, can be anything */
} PyFunctionObject;
-
PyFunctionObject
对象是Python
代码在运行时动态产生的,准确的说是在执行了一个def
语句产生的。 - 函数的所有静态信息保存在
PyFunctionObject.func_code
中,其实它就是一个PyCodeObject
对象,也就是函数编译后的结果。 -
PyFunctionObject
对象中含包含了一些函数在执行时必须的动态信息(上下文信息):-
func_globals
:函数在执行时关联的global
作用域 - ......
-
- 一段函数代码对应的
PyCodeObject
对象只有一个,但是对应的PyFunctionObject
对象可能有多个(一个函数多次调用)
0x02 无参函数调用
##########################
def f():
print "sssssssssss"
f()
##########################
# def f()对应的字节码
1 0 LOAD_CONST 0 (<code object f at 00000000041A6CB0, file "test3.py", line 1>)
3 MAKE_FUNCTION 0
6 STORE_NAME 0 (f)
# print "sssssssssss"对应的字节码
2 0 LOAD_CONST 1 ('sssssssssss')
3 PRINT_ITEM
4 PRINT_NEWLINE
5 LOAD_CONST 0 (None)
8 RETURN_VALUE
# f()对应的字节码
4 9 LOAD_NAME 0 (f)
12 CALL_FUNCTION 0
15 POP_TOP
16 LOAD_CONST 1 (None)
19 RETURN_VALUE
函数对象创建
Python
虚拟机在遇到def f()
语句时,会创建一个PyFunctionObject
对象,是在MAKE_FUNCTION
字节码指令中实现的。
PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
for(;;){
......
case MAKE_FUNCTION:
// 获得与函数f对应的PyCodeObject对象
v = POP(); /* code object */
x = PyFunction_New(v, f->f_globals);
Py_DECREF(v);
/* XXX Maybe this should be a separate opcode? */
if (x != NULL && oparg > 0) {
v = PyTuple_New(oparg);
if (v == NULL) {
Py_DECREF(x);
x = NULL;
break;
}
while (--oparg >= 0) {
w = POP();
PyTuple_SET_ITEM(v, oparg, w);
}
err = PyFunction_SetDefaults(x, v);
Py_DECREF(v);
}
PUSH(x);
break;
......
}
}
-
MAKE_FUNCTION
指令之前,LOAD_CONST
指令会将函数代码块编译成的PyCodeObject
对象压入到运行时栈中,MAKE_FUNCTION
中POP()
出该对象,然后以该对象和当前PyFrameObject
对象中的global
名字空间f_globals
作为参数,通过PyFunction_New()
函数创建一个新的PyFunctionObject
对象,这个f_globals
将作为函数运行期间的global
名字空间。
PyObject *
PyFunction_New(PyObject *code, PyObject *globals)
{
// 申请PyFunctionObject对象所需的内存空间
PyFunctionObject *op = PyObject_GC_New(PyFunctionObject,
&PyFunction_Type);
static PyObject *__name__ = 0;
if (op != NULL) {
// 初始化PyFunctionObject对象中各个域
PyObject *doc;
PyObject *consts;
PyObject *module;
op->func_weakreflist = NULL;
Py_INCREF(code);
op->func_code = code;
Py_INCREF(globals);
op->func_globals = globals;
op->func_name = ((PyCodeObject *)code)->co_name;
Py_INCREF(op->func_name);
op->func_defaults = NULL; /* No default arguments */
op->func_closure = NULL;
consts = ((PyCodeObject *)code)->co_consts;
if (PyTuple_Size(consts) >= 1) {
doc = PyTuple_GetItem(consts, 0);
if (!PyString_Check(doc) && !PyUnicode_Check(doc))
doc = Py_None;
}
else
doc = Py_None;
Py_INCREF(doc);
op->func_doc = doc;
op->func_dict = NULL;
op->func_module = NULL;
if (!__name__) {
__name__ = PyString_InternFromString("__name__");
if (!__name__) {
Py_DECREF(op);
return NULL;
}
}
module = PyDict_GetItem(globals, __name__);
if (module) {
Py_INCREF(module);
op->func_module = module;
}
}
else
return NULL;
_PyObject_GC_TRACK(op);
return (PyObject *)op;
}
函数调用
CALL_FUNCTION
字节码指令就是函数的调用指令。
PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
for(;;){
......
case CALL_FUNCTION:
{
PyObject **sp;
PCALL(PCALL_ALL);
sp = stack_pointer;
x = call_function(&sp, oparg);
stack_pointer = sp;
PUSH(x);
if (x != NULL)
continue;
break;
}
......
}
}
Python
虚拟机在执行CALL_FUNCTION
字节码指令的时候,获取了运行时栈的栈顶指针,然后就交给call_function()
函数处理了。
static PyObject *
call_function(PyObject ***pp_stack, int oparg)
{
// 处理函数参数信息
int na = oparg & 0xff;
int nk = (oparg>>8) & 0xff;
int n = na + 2 * nk;
// 获得PyFunctionObject对象
PyObject **pfunc = (*pp_stack) - n - 1;
PyObject *func = *pfunc;
PyObject *x, *w;
/* Always dispatch PyCFunction first, because these are
presumed to be the most frequent callable object.
*/
if (PyCFunction_Check(func) && nk == 0) {
int flags = PyCFunction_GET_FLAGS(func);
PyThreadState *tstate = PyThreadState_GET();
PCALL(PCALL_CFUNCTION);
if (flags & (METH_NOARGS | METH_O)) {
PyCFunction meth = PyCFunction_GET_FUNCTION(func);
PyObject *self = PyCFunction_GET_SELF(func);
if (flags & METH_NOARGS && na == 0) {
C_TRACE(x, (*meth)(self,NULL));
}
else if (flags & METH_O && na == 1) {
PyObject *arg = EXT_POP(*pp_stack);
C_TRACE(x, (*meth)(self,arg));
Py_DECREF(arg);
}
else {
err_args(func, flags, na);
x = NULL;
}
}
else {
PyObject *callargs;
callargs = load_args(pp_stack, na);
READ_TIMESTAMP(*pintr0);
C_TRACE(x, PyCFunction_Call(func,callargs,NULL));
READ_TIMESTAMP(*pintr1);
Py_XDECREF(callargs);
}
} else {
if (PyMethod_Check(func) && PyMethod_GET_SELF(func) != NULL) {
/* optimize access to bound methods */
PyObject *self = PyMethod_GET_SELF(func);
PCALL(PCALL_METHOD);
PCALL(PCALL_BOUND_METHOD);
Py_INCREF(self);
func = PyMethod_GET_FUNCTION(func);
Py_INCREF(func);
Py_DECREF(*pfunc);
*pfunc = self;
na++;
n++;
} else
Py_INCREF(func);
READ_TIMESTAMP(*pintr0);
// 对PyFunctionObject对象进行调用
if (PyFunction_Check(func))
x = fast_function(func, pp_stack, n, na, nk);
else
x = do_call(func, pp_stack, na, nk);
READ_TIMESTAMP(*pintr1);
Py_DECREF(func);
}
while ((*pp_stack) > pfunc) {
w = EXT_POP(*pp_stack);
Py_DECREF(w);
PCALL(PCALL_POP);
}
return x;
}
- 变量
n
表示的是运行时栈中,栈顶的多少元素是与参数相关的 - 运行时栈中的函数指针通过
(*pp_stack) - (na + 2 * nk) - 1
拿到,pp_stack
是栈顶指针,减去所有参数的偏移,再减1
,就拿到了函数指针 - 上面代码中
PyFunction_Check(func)
检查后,就会进入fast_function()
函数中。
static PyObject *
fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
{
PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
PyObject *globals = PyFunction_GET_GLOBALS(func);
PyObject *argdefs = PyFunction_GET_DEFAULTS(func);
PyObject **d = NULL;
int nd = 0;
PCALL(PCALL_FUNCTION);
PCALL(PCALL_FAST_FUNCTION);
// 一般函数的快速通道
if (argdefs == NULL && co->co_argcount == n && nk==0 &&
co->co_flags == (CO_OPTIMIZED | CO_NEWLOCALS | CO_NOFREE)) {
PyFrameObject *f;
PyObject *retval = NULL;
PyThreadState *tstate = PyThreadState_GET();
PyObject **fastlocals, **stack;
int i;
PCALL(PCALL_FASTER_FUNCTION);
assert(globals != NULL);
/* XXX Perhaps we should create a specialized
PyFrame_New() that doesn't take locals, but does
take builtins without sanity checking them.
*/
assert(tstate != NULL);
f = PyFrame_New(tstate, co, globals, NULL);
if (f == NULL)
return NULL;
fastlocals = f->f_localsplus;
stack = (*pp_stack) - n;
for (i = 0; i < n; i++) {
Py_INCREF(*stack);
fastlocals[i] = *stack++;
}
retval = PyEval_EvalFrameEx(f,0);
++tstate->recursion_depth;
Py_DECREF(f);
--tstate->recursion_depth;
return retval;
}
if (argdefs != NULL) {
d = &PyTuple_GET_ITEM(argdefs, 0);
nd = ((PyTupleObject *)argdefs)->ob_size;
}
return PyEval_EvalCodeEx(co, globals,
(PyObject *)NULL, (*pp_stack)-n, na,
(*pp_stack)-2*nk, nk, d, nd,
PyFunction_GET_CLOSURE(func));
}
- 进入
fast_function()
函数以后,先取出PyFunctionObject
对象中保存的PyCodeObject
对象和函数运行时的global
名字空间等信息。 - 然后根据是“参数形式”分成两个分支:
- 无参数的情况下会进入一般函数的通道,然后
Python
虚拟机会创建新的PyFrameObject
对象,然后调用PyEval_EvalFrameEx
(伪CPU
)函数执行 - 另一条路径会进入
PyEval_EvalCodeEx()
函数,最终还是会调用到PyEval_EvalFrameEx
(伪CPU
)中
- 无参数的情况下会进入一般函数的通道,然后
- 从
PyEval_EvalFrameEx
(伪CPU
)开始,才算是真正进行了“函数调用”的状态(实际上就是对x86
机器函数调用的模拟:创建新的栈帧,在新的栈帧中执行代码。)。 - 在最终执行
PyEval_EvalFrameEx
(伪CPU
)函数时,PyFrameObject
对象已经失去了作用,它的作用只是将PyCodeObject
对象和函数需要的global
名字空间打包输送过来,然后叫交给PyFrameObject
(栈帧)对象。
0x03 函数执行时的名字空间
函数的PyFrameObject
(栈帧)中的global
名字空间就是在调用CALL_FUNCTION
字节码指令时,调用PyFunction_New()
时创建PyFunctionObject
对象时传入的参数,这个global
参数其实就是当前PyFrameObject
(栈帧)的global
名字空间。
因为有了这个机制,所以在函数内部就可以调用函数外的变量了。
0x04 函数参数的实现
参数类别
f(a, b, *list, name='python', **keys)
- 位置参数(
positional argument
):a
、b
- 键参数(
key argument
):name='python'
- 扩展位置参数(
excess positional argument
):*list
- 扩展键参数(
excess key argument
):**keys
static PyObject *
call_function(PyObject ***pp_stack, int oparg)
{
// 处理函数参数信息
int na = oparg & 0xff;
int nk = (oparg>>8) & 0xff;
int n = na + 2 * nk;
// 获得PyFunctionObject对象
PyObject **pfunc = (*pp_stack) - n - 1;
PyObject *func = *pfunc;
PyObject *x, *w;
......
return x;
}
在执行CALL_FUNCTION
字节码指令时,会获得一个指令参数oparg
,这个参数记录的是函数参数的个数(位置参数和键参数的个数)。
CALL_FUNCTION
字节码指令参数的长度是2
个字节,低字节记录的是位置参数的个数,高字节记录着键参数的个数。因此理论上,Python
中函数参数只能有256
个位置参数和256
个键参数。
在上面的call_function
代码中,na
就是位置参数的个数,nk
就是键参数的个数。
函数对应的PyCodeObject
对象的中co_nlocals
表示局部变量的个数,co_argcount
表示函数一共有多少参数。

上图可以看出,函数参数是位置参数还是键参数是由函数的实参形式决定的,而与函数定义时的形参没有任何关系。
例1
和例2
中的n
为什么不一样?因为n
表示运行时栈中,函数指针距离栈顶的距离,键参数会会将key
和value
一起压入运行时栈,所以计算公式为:n = na + 2 * nk
例3
中的co_argcount=2
,co_nlocals=3
,因为函数内部将*list
作为了一个局部变量,所以函数参数个数co_argcount=2
,局部变量co_nlocals=3
。函数内部将list
作为一个PyListObject
对象来保存扩展位置参数。
例4
同例3
。
位置参数的传递
#######################
def f(name, age):
age += 5
print name, age
age = 5
print age
f("cs", age)
print age
#######################
# def f(name, age):
1 0 LOAD_CONST 0 (<code object f at 00000000041EE330, file "test3.py", line 1>)
3 MAKE_FUNCTION 0
6 STORE_NAME 0 (f)
# age += 5
2 0 LOAD_FAST 1 (age)
3 LOAD_CONST 1 (5)
6 INPLACE_ADD
7 STORE_FAST 1 (age)
# print name, age
3 10 LOAD_FAST 0 (name)
13 PRINT_ITEM
14 LOAD_FAST 1 (age)
17 PRINT_ITEM
18 PRINT_NEWLINE
19 LOAD_CONST 0 (None)
22 RETURN_VALUE
# age = 5
5 9 LOAD_CONST 1 (5)
12 STORE_NAME 1 (age)
# print age
6 15 LOAD_NAME 1 (age)
18 PRINT_ITEM
19 PRINT_NEWLINE
# f("cs", age)
8 20 LOAD_NAME 0 (f)
23 LOAD_CONST 2 ('cs')
26 LOAD_NAME 1 (age)
29 CALL_FUNCTION 2
32 POP_TOP
# print age
10 33 LOAD_NAME 1 (age)
36 PRINT_ITEM
37 PRINT_NEWLINE
38 LOAD_CONST 3 (None)
41 RETURN_VALUE
- 与无参函数不同的是,上面例子中在
CALL_FUNCTION
字节码指令之前多了3
个LOAD
指令。可看出,函数需要的参数也被压入运行时栈中。接下来CALL_FUNCTION
字节码指令的参数oparg
为2
。
static PyObject *
fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
{
......
// 创建当前函数的栈帧
f = PyFrame_New(tstate, co, globals, NULL);
if (f == NULL)
return NULL;
fastlocals = f->f_localsplus;
stack = (*pp_stack) - n;
// 将参数拷贝到栈帧的f_localsplus域
for (i = 0; i < n; i++) {
Py_INCREF(*stack);
fastlocals[i] = *stack++;
}
......
}
- 创建完新的栈帧后,然后将运行时栈的内的函数参数拷贝到
f_localsplus
域中。 - 回忆之前的执行环境(
PyFrameObject
),函数参数使用的内存在f_localsplus
中除了“运行时栈”以外的空间中。所以在还没有开始执行代码(PyEval_EvalFrameEx
),函数的参数已经维护在栈帧中了。
函数PyFrameObject栈帧对象
位置参数的访问
接下来就开始使用伪CPU
(PyEval_EvalFrameEx()
)来执行字节码指令。
#define GETLOCAL(i) (fastlocals[i])
#define SETLOCAL(i, value) do { PyObject *tmp = GETLOCAL(i); \
GETLOCAL(i) = value; \
Py_XDECREF(tmp); } while (0)
PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
for(;;){
......
case LOAD_FAST:
x = GETLOCAL(oparg);
if (x != NULL) {
Py_INCREF(x);
PUSH(x);
goto fast_next_opcode;
}
format_exc_check_arg(PyExc_UnboundLocalError,
UNBOUNDLOCAL_ERROR_MSG,
PyTuple_GetItem(co->co_varnames, oparg));
break;
case STORE_FAST:
v = POP();
SETLOCAL(oparg, v);
goto fast_next_opcode;
......
}
}
-
LOAD_FAST
和STORE_FAST
字节码指令都是对f_localsplus
的操作。
总结(函数参数的传递和访问):
- 在进行函数调用(
CALL_FUNCTION
)之前,Python
将函数参数从左到右压入运行时栈,然后在fast_function()
中,又将这些参数拷贝到与函数对应的PyFrameObject
(栈帧)中的f_localsplus
域中。 - 访问函数参数时,
Python
没有去查名字空间,而是直接通过索引来访问f_localsplus
中存储的符号对应的值。这种通过索引进行访问的方法也正是“位置参数”名称的由来。
函数调用过程中参数的变化序列
位置参数的默认值
带默认参数值的函数,def
语句编译后的字节码指令如下所示:
# def f(name='yl', age=18):
1 0 LOAD_CONST 0 ('yl')
3 LOAD_CONST 1 ('18')
6 LOAD_CONST 2 (<code object f at 00000000041EEEB0, file "test3.py", line 1>)
9 MAKE_FUNCTION 2
12 STORE_NAME 0 (f)
然后执行MAKE_FUNCTION
字节码指令的时候,参数为2
(无参函数或参数无默认值的函数执行MAKE_FUNCTION
指令时参数为0
)
PyObject *
PyEval_EvalFrameEx(PyFrameObject *f, int throwflag)
{
for(;;){
......
case MAKE_FUNCTION:
// 获得与函数f对应的PyCodeObject对象
v = POP(); /* code object */
// 创建PyFunctionObject对象
x = PyFunction_New(v, f->f_globals);
Py_DECREF(v);
// 处理函数参数默认值
if (x != NULL && oparg > 0) {
v = PyTuple_New(oparg);
if (v == NULL) {
Py_DECREF(x);
x = NULL;
break;
}
while (--oparg >= 0) {
w = POP();
PyTuple_SET_ITEM(v, oparg, w);
}
err = PyFunction_SetDefaults(x, v);
Py_DECREF(v);
}
PUSH(x);
break;
......
}
}
- 创建完
PyFunctionObject
对象以后,会将所有默认值从运行时栈中弹出,装到PyTupleObject
对象中,然后将PyTupleObject
对象通过PyFunction_SetDefaults()
函数,设置成PyFunctionObject
对象的func_defaults
域的值。 - 这样
PyCodeObject
对象、global
名字空间和函数参数默认值func_defaults
都被PyFunctionObject
对象携带捆绑在一起。
f()方式调用
static PyObject *
fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
{
PyCodeObject *co = (PyCodeObject *)PyFunction_GET_CODE(func);
PyObject *globals = PyFunction_GET_GLOBALS(func);
// 获取函数对应的PyFunctionObject对象中的func_defaults
PyObject *argdefs = PyFunction_GET_DEFAULTS(func);
PyObject **d = NULL;
int nd = 0;
PCALL(PCALL_FUNCTION);
PCALL(PCALL_FAST_FUNCTION);
......
if (argdefs != NULL) {
// 获得函数参数默认值信息(第一个默认值的地址;默认值的个数)
d = &PyTuple_GET_ITEM(argdefs, 0);
nd = ((PyTupleObject *)argdefs)->ob_size;
}
return PyEval_EvalCodeEx(co, globals,
(PyObject *)NULL, (*pp_stack)-n, na, // 位置参数的信息
(*pp_stack)-2*nk, nk, // 键参数信息
d, nd, // 函数默认参数信息
PyFunction_GET_CLOSURE(func));
}

由于有默认值的PyFunctionObject
对象argdefs != NULL
,所以代码会进入PyEval_EvalCodeEx()
函数,将默认参数取出作为参数传递给PyEval_EvalCodeEx()
函数。
PyObject *
PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
PyObject **args, int argcount, PyObject **kws, int kwcount,
PyObject **defs, int defcount, PyObject *closure)
{
register PyFrameObject *f;
register PyObject *retval = NULL;
register PyObject **fastlocals, **freevars;
PyThreadState *tstate = PyThreadState_GET();
PyObject *x, *u;
// 创建新的PyFrameObject对象
f = PyFrame_New(tstate, co, globals, locals);
fastlocals = f->f_localsplus;
freevars = f->f_localsplus + co->co_nlocals;
if (co->co_argcount > 0 || co->co_flags & (CO_VARARGS | CO_VARKEYWORDS)) {
int i;
// n为CALL_FUNCTION的参数指示的传入的位置参数的个数,即na
int n = argcount;
......
// 判断是否使用参数的默认值
if (argcount < co->co_argcount) {
// m = 位置参数总数 - 被设置了默认值的位置参数个数
int m = co->co_argcount - defcount;
// 函数调用者必须传递一般位置参数的参数值
for (i = argcount; i < m; i++) {
if (GETLOCAL(i) == NULL) {
goto fail;
}
}
// n>m意味着调用者希望替换一些默认位置参数的默认值
if (n > m)
i = n - m;
else
i = 0;
// 设置默认位置参数的默认值
for (; i < defcount; i++) {
if (GETLOCAL(m+i) == NULL) {
PyObject *def = defs[i];
Py_INCREF(def);
SETLOCAL(m+i, def);
}
}
}
}
retval = PyEval_EvalFrameEx(f,0);
return retval;
}
- 默认位置参数是指定了默认值的位置参数;一般位置参数是没有指定默认值的位置参数
-
argcount < co->co_argcount
用来判断是否需要使用参数的默认值,当调用函数传递的位置参数的个数小于函数编译后的PyCodeObject
对象中co_argcount
指定的参数个数时,则表示Python
虚拟机需要为函数设置默认参数。 -
m = co->co_argcount - defcount
中,m
表示一般位置参数的个数 -
i = n - m
来确定从哪个默认位置参数开始设定参数的默认值(def g(a, b, c=1, d=2)
,g(3, 3, 3)
),这个例子中,n=3
(函数调用时传递的位置参数的个数),m=2
(一般位置参数的个数),所以n-m=1
(表示从第一个位置参数的地方开始设置默认值,即为d=2
设置默认值) - 然后从
PyFrameObject
对象的func_defaults
中将这些参数取出,通过SETLOCAL
将其放入PyFrameObject
对象的f_localsplus
所管理的内存块中。i
表示需要在f_localsplus
中设置默认值的位置,它从第一个需要设置默认值的默认参数位置开始,依次向后。
f(age=10)方式调用

在进入fast_function()
时,仍然不会进入快速通道。最后还是进入PyEval_EvalCodeEx()
函数中,其中argcount就是na
,kwcount
就是nk
。

与f()
的调用相比,我们关心的是age=10
是如何替换默认参数的。
- 核心算法就是:在编译时,
Python
编译器会将函数的def
语句中出现的参数名称都记录在变量co_varname
中。调用f(age=10)
时(CALL_FUNCTION
字节码指令),会将age
与10
都压入运行时栈中,在PyEval_EvalCodeEx()
函数中,就从运行时栈中拿出键参数的名称,在co_varname
中查找,查找到以后,根据查找到的参数索引,直接设置f_localsplus
中的内存值为10
,这为默认参数位置设置了函数调用者希望的值。 - 上面查找到的索引为什么可以直接用来设置
f_localsplus
的内存呢?- 因为编译得到的
co_varname
与f_localsplus
中存放参数的顺序都是按照def
语句定义的顺序进行排放的,所以可以直接使用位置参数的索引。
- 因为编译得到的
扩展位置参数和扩展键参数
扩展位置参数(*list
)和扩展建参数(**kwargs
),实际上是作为函数的局部变量来实现的。
当Python
在编译一个函数时,如果发现其形式参数中存在*list
这样的扩展位置参数,那么Python
会在编译所得的PyCodeObject
对象的co_flags
中添加一个标识符CO_VARARGS
,表示该函数在被调用时需要处理扩展位置参数;同理扩展键参数(**kwargs
)会添加CO_VARKEYWORDS
标识。
处理完正规位置参数以后,就开始处理扩展位置参数,将所有扩展位置参数加入到PyTupleObject
对象中,然后放入到f_localsplus
内存中,它的内存位置就在正规位置参数的下一个位置。
对于扩展键参数,会创建一个PyDictObject
对象,对所有的键参数进行过滤,将扩展键参数插入到PyDictObject
对象中,扩展键参数PyDictObject
被插入到f_localsplus
内存中扩展位置参数PyTupleObject
的下一个位置。

0x05 函数中局部变量的访问
按照之前的思维,当需要访问局部变量时,应该到local
名字空间中去搜索变量名。但是,在调用函数期间,局部变量和函数参数一样,存储在f_localsplus
中。
为什么函数中没有local
名字空间?
- 因为函数中的局部变量总是固定不变的。所有在编译的时候就能确定局部变量使用的内存空间的位置,也能确定访问局部变量的字节码指令应该如何访问内存。
- 函数中,
Python
可以使用静态的方法来实现局部变量,而不需要借助于动态的查找PyDictObject
对象的技术,毕竟静态方法可以极大的提高函数执行的效率。
0x06 嵌套函数、闭包与decorator
名字空间是在运行时由
Python
虚拟机动态维护的,但是有时候我们需要将名字空间静态化。换句话说就是,我们希望有的代码不受名字空间变换的影响,始终保持一致的行为和结果。使用闭包(
closure
)来实现这种场景:内嵌函数被返回的时候,会将使用到的名字空间和函数捆绑在一起,然后返回,这就形成了一个闭包。闭包是最内嵌套规则实现方式,不使用闭包也能实现最内嵌套规则。

实现闭包的基石
闭包的创建通常是利用嵌套函数来完成的。在PyCodeObject
中,与嵌套函数相关的属性是co_cellvars
和co_freevars
。
-
co_cellvars
:通常是一个tuple
,保存嵌套的作用域中使用的变量名集合 -
co_freevars
:通常也是一个tuple
,保存使用了的外层作用域中的变量名集合
# example01.py
def get_func():
value = "inner"
def inner_func():
print value
return inner_func
show_value = get_func()
show_value()
上例中会产生3
个PyCodeObject
对象,与get_func
对应的PyCodeObject
对象中的co_cellvars
就应该包含字符串value
,因为其嵌套作用域(inner_func
的作用域)中使用了这个符号。
同理,与函数inner_func
对应的PyCodeObject
对象中的co_freevars
中应该也有字符串value
。
PyFrameObject
对象中,也有一个属性与闭包的实现相关,就是f_localsplus
。PyFrame_New
函数中的代码extras = code->co_stacksize + code->co_nlocals + ncells + nfrees
,表示了f_localsplus
属于4
个元素:运行时栈、局部变量、cell
(co_cellvars
)对象和free
(co_freevars
)对象。

闭包的实现
# example01.py的字节码指令
# def get_func():
1 0 LOAD_CONST 0 (<code object get_func at 00000000041EEE30, file "test3.py", line 1>)
3 MAKE_FUNCTION 0
6 STORE_NAME 0 (get_func)
# value = "inner"
2 0 LOAD_CONST 1 ('inner')
3 STORE_DEREF 0 (value)
# def inner_func():
3 6 LOAD_CLOSURE 0 (value)
9 BUILD_TUPLE 1
12 LOAD_CONST 2 (<code object inner_func at 00000000041EECB0, file "test3.py", line 3>)
15 MAKE_CLOSURE 0
18 STORE_FAST 0 (inner_func)
# print value
4 0 LOAD_DEREF 0 (value)
3 PRINT_ITEM
4 PRINT_NEWLINE
5 LOAD_CONST 0 (None)
8 RETURN_VALUE
# return inner_func
5 21 LOAD_FAST 0 (inner_func)
24 RETURN_VALUE
# show_value = get_func()
7 9 LOAD_NAME 0 (get_func)
12 CALL_FUNCTION 0
15 STORE_NAME 1 (show_value)
# show_value()
8 18 LOAD_NAME 1 (show_value)
21 CALL_FUNCTION 0
24 POP_TOP
25 LOAD_CONST 1 (None)
28 RETURN_VALUE
创建closure
调用12 CALL_FUNCTION 0
时,会进入fast_function
函数,由于co_flag
为3
(CO_OPTIMIZED|CO_NEWLOCALS
),所以不会进入快速通道,最终调用到PyEval_EvalCodeEx
。
在PyEval_EvalCodeEx
中,Python
虚拟机会如同处理默认参数一样,将co_cellvars
中的东西拷贝到新创建的PyFrameObject
的f_localsplus
中。
PyObject *
PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
PyObject **args, int argcount, PyObject **kws, int kwcount,
PyObject **defs, int defcount, PyObject *closure)
{
for (i = 0; i < PyTuple_GET_SIZE(co->co_cellvars); ++i) {
// 获得被嵌套函数共享的符号名
cellname = PyString_AS_STRING(
PyTuple_GET_ITEM(co->co_cellvars, i));
found = 0;
for (j = 0; j < nargs; j++) {
argname = PyString_AS_STRING(
PyTuple_GET_ITEM(co->co_varnames, j));
if (strcmp(cellname, argname) == 0) {
c = PyCell_New(GETLOCAL(j));
if (c == NULL)
goto fail;
GETLOCAL(co->co_nlocals + i) = c;
found = 1;
break;
}
}
// 处理被嵌套函数共享外层函数的默认参数
if (found == 0) {
c = PyCell_New(NULL);
if (c == NULL)
goto fail;
SETLOCAL(co->co_nlocals + i, c);
}
}
}
其中found
变量表示:被内层嵌套函数引用的符号是否已经与某个值绑定的标识,或者说与某个对象已经建立约束的标识。只有在内层嵌套函数引用的是外层函数的一个有默认值的参数时,这个标识才可能为1
。
Python
虚拟机接下来会创建一个PyCellObject
对象,这个对象很简单,只维护了一个指针。
随后在value = "inner"
中被赋值,接下来将cell
对象拷贝到新创建的PyFrameObject
对象的f_localsplus
中,拷贝的位置在co->co_nlocals+i
(cell
对象的位置在局部变量之后)。
开始执行get_func
函数,3 STORE_DEREF 0 (value)
做的动作是,从运行时栈拿出inner
对象,从f_localsplus
中取出PyCellObject
对象,即对PyCellObject
对象赋值。

接下来就是形成闭包的关键,在执行def inner_func():
时,Python
虚拟机会将(value,"inner")
这个约束塞到PyFunctionObject
对象中。
TARGET(MAKE_CLOSURE)
{
// 获得PyCodeObject对象
v = POP(); /* code object */
// 绑定global名字空间
x = PyFunction_New(v, f->f_globals);
Py_DECREF(v);
if (x != NULL) {
// 获得tuple对象,其中包含PyCellObject对象
v = POP();
// 绑定约束集合
if (PyFunction_SetClosure(x, v) != 0) {
/* Can't happen unless bytecode is corrupt. */
why = WHY_EXCEPTION;
}
Py_DECREF(v);
}
PUSH(x);
break;
}
6 LOAD_CLOSURE 0 (value)
将PyCellObject
对象取出,放入运行时栈中,接下来9 BUILD_TUPLE 1
指令将PyCellObject
对象打包进一个tuple
对象中,最后通过15 MAKE_CLOSURE 0
指令完成约束与PyCodeObject
对象的绑定。
18 STORE_FAST 0 (inner_func)
将新创建的PyFunctionObject
对象放置到f_localsplus
中,最后再将新建的这个PyFunctionObject
对象压入运行时栈,然后返回。

使用closure
closure
是在get_func
中被创建,而对closure
的使用,则是在inner_func
中。
在执行show_value()
的CALL_FUNCTION
指令时,和inner_func
对应的PyCodeObject
对象中的co_flags
包含CO_NESTED
,所以最后还是进入PyEval_EvalCodeEx
中。
inner_func
编译后的PyCodeObject
对象中co_freevars
里面有引用的外层作用域中的符号名,所以需要进行处理,如下所示:
PyObject *
PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
PyObject **args, int argcount, PyObject **kws, int kwcount,
PyObject **defs, int defcount, PyObject *closure)
{
......
if (PyTuple_GET_SIZE(co->co_freevars)) {
int i;
for (i = 0; i < PyTuple_GET_SIZE(co->co_freevars); ++i) {
PyObject *o = PyTuple_GET_ITEM(closure, i);
Py_INCREF(o);
freevars[PyTuple_GET_SIZE(co->co_cellvars) + i] = o;
}
}
......
}
上面代码中closure
是fast_function
传进来的,其实就是在PyFunctionObject
对象中与PyCodeObject
对象绑定的装满了PyCellObject
对象的tuple
,所以在PyEval_EvalCodeEx
中,进行的动作就是将这个PyCellObject
对象一个一个放入到f_localsplus
中相应的位置。处理完closure
后,inner_func
对应的PyFrameObject
中的f_localsplus
如下所示:

在inner_func
调用的过程中,当引用外层作用域的符号时,一定是到f_localsplus
中的free
变量区域中获取对应的值。这正好对应的是print value
对应的字节码指令0 LOAD_DEREF 0 (value)
。
Decorator
decorator
只是closure
的一种包装形式,只是对外展示的语法格式不同而已。
欢迎关注微信公众号(coder0x00)或扫描下方二维码关注,我们将持续搜寻程序员必备基础技能包提供给大家。
网友评论